Skip to content

Commit ae98bbc

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 1032bfe commit ae98bbc

25 files changed

Lines changed: 1132 additions & 4 deletions
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
---
2+
name: new-event-source
3+
description: Add a new AWS event source attribute (e.g., Kinesis, Kafka, MQ) to the Lambda .NET Annotations framework, including the attribute class, source generator integration, CloudFormation writer, unit tests, writer tests, source generator tests, and integration tests
4+
---
5+
6+
# Adding a New Event Source to Lambda Annotations
7+
8+
This skill guides you through adding a complete new event source attribute to the AWS Lambda .NET Annotations framework. Use this when a user asks to add support for a new AWS event source like Kinesis, Kafka, MQ, etc.
9+
10+
## Prerequisites
11+
12+
Before starting, gather from the user:
13+
1. **Service name** (e.g., "Kinesis", "Kafka", "MQ")
14+
2. **Primary resource identifier** (e.g., stream ARN, topic ARN, broker ARN)
15+
3. **CloudFormation event type string** (e.g., "Kinesis", "MSK", "MQ")
16+
4. **Event class name** from the corresponding `Amazon.Lambda.*Events` NuGet package (e.g., `KinesisEvent`)
17+
5. **Optional properties** the attribute should support (e.g., BatchSize, StartingPosition, Filters)
18+
6. **Whether `@` references use `Fn::GetAtt` or `Ref`** — event source mappings use `Fn::GetAtt`, subscriptions use `Ref`
19+
20+
## Reference Examples
21+
22+
Read these files to understand existing patterns before creating new ones:
23+
- **SNS (simplest, subscription-based)**: `Libraries/src/Amazon.Lambda.Annotations/SNS/SNSEventAttribute.cs`
24+
- **SQS (event source mapping with batching)**: `Libraries/src/Amazon.Lambda.Annotations/SQS/SQSEventAttribute.cs`
25+
- **DynamoDB (stream-based)**: `Libraries/src/Amazon.Lambda.Annotations/DynamoDB/DynamoDBEventAttribute.cs`
26+
- **S3 (notification-based)**: `Libraries/src/Amazon.Lambda.Annotations/S3/S3EventAttribute.cs`
27+
28+
Also see `Libraries/src/Amazon.Lambda.Annotations/ADDING_NEW_EVENT_SOURCE.md` for the full detailed developer guide.
29+
30+
## Steps
31+
32+
### Step 1: Create the Event Attribute Class
33+
34+
**Create**: `Libraries/src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs`
35+
36+
Key patterns:
37+
- Add copyright header: `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.` + `// SPDX-License-Identifier: Apache-2.0`
38+
- Inherit from `Attribute` with `[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]`
39+
- Constructor takes the primary resource identifier as a required `string` parameter
40+
- All optional properties use nullable backing fields with `Is<PropertyName>Set` internal properties
41+
- Include auto-derived `ResourceName` property (strips `@` prefix or extracts name from ARN)
42+
- Include `internal List<string> Validate()` method with all validation rules
43+
- Use `Regex("^[a-zA-Z0-9]+$")` for ResourceName validation
44+
45+
### Step 2: Register Type Full Names
46+
47+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs`
48+
49+
Add constants:
50+
```csharp
51+
public const string {ServiceName}EventAttribute = "Amazon.Lambda.Annotations.{ServiceName}.{ServiceName}EventAttribute";
52+
public const string {ServiceName}Event = "Amazon.Lambda.{ServiceName}Events.{ServiceName}Event";
53+
```
54+
55+
Also add to `EventType` enum if needed in `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventType.cs`.
56+
57+
### Step 3: Create the Attribute Builder
58+
59+
**Create**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs`
60+
61+
Extracts attribute data from Roslyn `AttributeData`. Use consistent `else if` chaining. Reference: `SNSEventAttributeBuilder.cs`.
62+
63+
### Step 4: Register in AttributeModelBuilder
64+
65+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs`
66+
67+
Add `else if` block for the new attribute type after the existing event attribute blocks.
68+
69+
### Step 5: Register in EventTypeBuilder
70+
71+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs`
72+
73+
Add `else if` block mapping the attribute to the `EventType` enum value.
74+
75+
### Step 6: Add DiagnosticDescriptor
76+
77+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs`
78+
79+
Add descriptor with the next available `AWSLambda0XXX` ID for invalid attribute validation errors.
80+
81+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` — add the new diagnostic ID.
82+
83+
### Step 7: Add Validation in LambdaFunctionValidator
84+
85+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs`
86+
87+
1. Add `Validate{ServiceName}Events()` call in `ValidateFunction` method
88+
2. Create private `Validate{ServiceName}Events()` method that validates:
89+
- Attribute properties via `Validate()` method
90+
- Method parameters (first must be event type, optional second is `ILambdaContext`)
91+
- Return type (usually `void` or `Task`)
92+
93+
### Step 8: Add Dependency Check
94+
95+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs`
96+
97+
In `ValidateDependencies`, add check for `Amazon.Lambda.{ServiceName}Events` NuGet package.
98+
99+
### Step 9: Check SyntaxReceiver
100+
101+
**Check**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs`
102+
103+
Add the new attribute name if the SyntaxReceiver filters by attribute name strings.
104+
105+
### Step 10: Add CloudFormation Writer Logic
106+
107+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs`
108+
109+
1. Add `case AttributeModel<{ServiceName}EventAttribute>` in the event processing switch
110+
2. Create `Process{ServiceName}Attribute()` method that writes CF template properties
111+
- Event source mappings (SQS, DynamoDB, Kinesis): use `Fn::GetAtt` for `@` references
112+
- Subscription events (SNS): use `Ref` for `@` references
113+
- Track synced properties in metadata
114+
115+
### Step 11: Create Attribute Unit Tests
116+
117+
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs`
118+
119+
Cover: constructor, defaults, property tracking, ResourceName derivation, all validation paths. Reference: `SQSEventAttributeTests.cs`, `DynamoDBEventAttributeTests.cs`, `SNSEventAttributeTests.cs`.
120+
121+
### Step 12: Create CloudFormation Writer Tests
122+
123+
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs`
124+
125+
This is a `partial class CloudFormationWriterTests`. Include tests for:
126+
1. `Verify{ServiceName}EventAttributes_AreCorrectlyApplied` — Theory with JSON/YAML and property combinations
127+
2. `Verify{ServiceName}EventProperties_AreSyncedCorrectly` — Synced properties update when attributes change
128+
3. `SwitchBetweenArnAndRef_For{Resource}` — ARN to `@` reference switching
129+
4. `Verify{Resource}CanBeSet_FromCloudFormationParameter` — CF Parameters handling
130+
5. `VerifyManuallySet{ServiceName}EventProperties_ArePreserved` — Hand-edited template preservation
131+
132+
Reference: `SQSEventsTests.cs`, `DynamoDBEventsTests.cs`, `SNSEventsTests.cs`.
133+
134+
### Step 13: Create Valid Event Examples + Source Generator Test
135+
136+
**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt`
137+
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` (generated handler snapshots)
138+
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template`
139+
**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyValid{ServiceName}Events()` test
140+
141+
### Step 14: Create Invalid Event Examples + Source Generator Test
142+
143+
**Create**: `Libraries/test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error`
144+
145+
Cover: invalid property values, invalid params, invalid return type, multiple events, invalid ARN, invalid resource name.
146+
147+
**Modify**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` — add `VerifyInvalid{ServiceName}Events_ThrowsCompilationErrors()` test with diagnostic assertions including line spans.
148+
149+
### Step 15: Create Generated Code Snapshots
150+
151+
**Create**: `Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/`
152+
153+
Tip: Run the source generator once to get actual output, then use as snapshot.
154+
155+
### Step 16: Create Integration Test
156+
157+
**Create**: `Libraries/test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs`
158+
**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` — resource lookup
159+
**Modify**: `Libraries/test/TestServerlessApp.IntegrationTests/DeploymentScript.ps1` — if needed
160+
161+
### Step 17: Update AnalyzerReleases.Unshipped.md
162+
163+
**Modify**: `Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md`
164+
165+
## File Map Summary
166+
167+
| Action | File Path |
168+
|--------|-----------|
169+
| Create | `src/Amazon.Lambda.Annotations/{ServiceName}/{ServiceName}EventAttribute.cs` |
170+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs` |
171+
| Create | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/{ServiceName}EventAttributeBuilder.cs` |
172+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs` |
173+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs` |
174+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs` |
175+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs` |
176+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs` |
177+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs` |
178+
| Modify | `src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/AnalyzerReleases.Unshipped.md` |
179+
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/{ServiceName}EventAttributeTests.cs` |
180+
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/{ServiceName}EventsTests.cs` |
181+
| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Valid{ServiceName}Events.cs.txt` |
182+
| Create | `test/TestServerlessApp/{ServiceName}EventExamples/Invalid{ServiceName}Events.cs.error` |
183+
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/{ServiceName}/` |
184+
| Create | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/Snapshots/ServerlessTemplates/{serviceName}Events.template` |
185+
| Modify | `test/Amazon.Lambda.Annotations.SourceGenerators.Tests/SourceGeneratorTests.cs` |
186+
| Create | `test/TestServerlessApp.IntegrationTests/{ServiceName}EventSourceMapping.cs` |
187+
| Modify | `test/TestServerlessApp.IntegrationTests/IntegrationTestContextFixture.cs` |
188+
189+
## Important Conventions
190+
191+
- **Copyright header** on every new `.cs` file
192+
- **Consistent `else if` chaining** in attribute builders (never `if` then `if` for the same loop)
193+
- **Both JSON and YAML** template formats must be tested in writer tests
194+
- **Invalid event test spans** must reference exact line numbers in the `.cs.error` file
195+
- **`.cs.txt` extension** for valid test files (prevents deployment)
196+
- **`.cs.error` extension** for invalid test files (prevents compilation)
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/AnalyzerReleases.Unshipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB fun
2323
AWSLambda0136 | AWSLambdaCSharpGenerator | Error | Invalid S3EventAttribute
2424
AWSLambda0137 | AWSLambdaCSharpGenerator | Error | Invalid DynamoDBEventAttribute
2525
AWSLambda0138 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute
26+
AWSLambda0139 | AWSLambdaCSharpGenerator | Error | Invalid ScheduleEventAttribute

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,5 +295,12 @@ public static class DiagnosticDescriptors
295295
category: "AWSLambdaCSharpGenerator",
296296
DiagnosticSeverity.Error,
297297
isEnabledByDefault: true);
298+
299+
public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0139",
300+
title: "Invalid ScheduleEventAttribute",
301+
messageFormat: "Invalid ScheduleEventAttribute encountered: {0}",
302+
category: "AWSLambdaCSharpGenerator",
303+
DiagnosticSeverity.Error,
304+
isEnabledByDefault: true);
298305
}
299306
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using Amazon.Lambda.Annotations.ALB;
6+
using Amazon.Lambda.Annotations.Schedule;
67
using Amazon.Lambda.Annotations.APIGateway;
78
using Amazon.Lambda.Annotations.DynamoDB;
9+
using Amazon.Lambda.Annotations.SNS;
810
using Amazon.Lambda.Annotations.S3;
911
using Amazon.Lambda.Annotations.SQS;
1012
using Microsoft.CodeAnalysis;
@@ -132,6 +134,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
132134
Type = TypeModelBuilder.Build(att.AttributeClass, context)
133135
};
134136
}
137+
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ScheduleEventAttribute), SymbolEqualityComparer.Default))
138+
{
139+
var data = ScheduleEventAttributeBuilder.Build(att);
140+
model = new AttributeModel<ScheduleEventAttribute>
141+
{
142+
Data = data,
143+
Type = TypeModelBuilder.Build(att.AttributeClass, context)
144+
};
145+
}
135146
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.HttpApiAuthorizerAttribute), SymbolEqualityComparer.Default))
136147
{
137148
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
4242
{
4343
events.Add(EventType.SNS);
4444
}
45+
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ScheduleEventAttribute)
46+
{
47+
events.Add(EventType.Schedule);
48+
}
4549
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
4650
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
4751
{
@@ -56,4 +60,4 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
5660
return events;
5761
}
5862
}
59-
}
63+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
2929
{ "ALBApiAttribute", "ALBApi" },
3030
{ "S3EventAttribute", "S3Event" },
3131
{ "DynamoDBEventAttribute", "DynamoDBEvent" },
32-
{ "SNSEventAttribute", "SNSEvent" }
32+
{ "SNSEventAttribute", "SNSEvent" },
33+
{ "ScheduleEventAttribute", "ScheduleEvent" }
3334
};
3435

3536
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
@@ -68,6 +68,9 @@ public static class TypeFullNames
6868
public const string SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent";
6969
public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute";
7070

71+
public const string ScheduledEvent = "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent";
72+
public const string ScheduleEventAttribute = "Amazon.Lambda.Annotations.Schedule.ScheduleEventAttribute";
73+
7174
public const string LambdaSerializerAttribute = "Amazon.Lambda.Core.LambdaSerializerAttribute";
7275
public const string DefaultLambdaSerializer = "Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer";
7376

@@ -99,7 +102,8 @@ public static class TypeFullNames
99102
ALBApiAttribute,
100103
S3EventAttribute,
101104
DynamoDBEventAttribute,
102-
SNSEventAttribute
105+
SNSEventAttribute,
106+
ScheduleEventAttribute
103107
};
104108
}
105109
}

0 commit comments

Comments
 (0)