Skip to content
Open
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
2 changes: 1 addition & 1 deletion .agents/skills/new-event-source/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Read these files to understand existing patterns before creating new ones:

Key patterns:
- Add copyright header: `// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.` + `// SPDX-License-Identifier: Apache-2.0`
- Inherit from `Attribute` with `[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]`
- Inherit from `Attribute` with `[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]` (use `AllowMultiple = false` for event types where multiple triggers on the same function don't make sense, e.g., Schedule events)
- Constructor takes the primary resource identifier as a required `string` parameter
- All optional properties use nullable backing fields with `Is<PropertyName>Set` internal properties
- Include auto-derived `ResourceName` property (strips `@` prefix or extracts name from ARN)
Expand Down
11 changes: 11 additions & 0 deletions .autover/changes/add-scheduleevent-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 [ScheduleEvent] annotation attribute for declaratively configuring schedule-triggered Lambda functions with support for rate and cron expressions, description, input, and enabled state."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ AWSLambda0135 | AWSLambdaCSharpGenerator | Error | Unmapped parameter on ALB fun
AWSLambda0136 | AWSLambdaCSharpGenerator | Error | Invalid S3EventAttribute
AWSLambda0137 | AWSLambdaCSharpGenerator | Error | Invalid DynamoDBEventAttribute
AWSLambda0138 | AWSLambdaCSharpGenerator | Error | Invalid SNSEventAttribute
AWSLambda0139 | AWSLambdaCSharpGenerator | Error | Invalid ScheduleEventAttribute
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,12 @@ public static class DiagnosticDescriptors
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0139",
title: "Invalid ScheduleEventAttribute",
messageFormat: "Invalid ScheduleEventAttribute encountered: {0}",
category: "AWSLambdaCSharpGenerator",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
Comment thread
GarrettBeatty marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.Schedule;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -132,6 +134,15 @@ public static AttributeModel Build(AttributeData att, GeneratorExecutionContext
Type = TypeModelBuilder.Build(att.AttributeClass, context)
};
}
else if (att.AttributeClass.Equals(context.Compilation.GetTypeByMetadataName(TypeFullNames.ScheduleEventAttribute), SymbolEqualityComparer.Default))
{
var data = ScheduleEventAttributeBuilder.Build(att);
model = new AttributeModel<ScheduleEventAttribute>
{
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,47 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Annotations.Schedule;
using Microsoft.CodeAnalysis;
using System;

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

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.Description) && pair.Value.Value is string description)
{
data.Description = description;
}
else if (pair.Key == nameof(data.Input) && pair.Value.Value is string input)
{
data.Input = input;
}
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 @@ -42,6 +42,10 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
{
events.Add(EventType.SNS);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ScheduleEventAttribute)
{
events.Add(EventType.Schedule);
}
else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute)
{
Expand All @@ -56,4 +60,4 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
return events;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "ALBApiAttribute", "ALBApi" },
{ "S3EventAttribute", "S3Event" },
{ "DynamoDBEventAttribute", "DynamoDBEvent" },
{ "SNSEventAttribute", "SNSEvent" }
{ "SNSEventAttribute", "SNSEvent" },
{ "ScheduleEventAttribute", "ScheduleEvent" }
};

public List<MethodDeclarationSyntax> LambdaMethods { get; } = new List<MethodDeclarationSyntax>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ public static class TypeFullNames
public const string SNSEvent = "Amazon.Lambda.SNSEvents.SNSEvent";
public const string SNSEventAttribute = "Amazon.Lambda.Annotations.SNS.SNSEventAttribute";

public const string ScheduledEvent = "Amazon.Lambda.CloudWatchEvents.ScheduledEvents.ScheduledEvent";
public const string ScheduleEventAttribute = "Amazon.Lambda.Annotations.Schedule.ScheduleEventAttribute";

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

Expand Down Expand Up @@ -99,7 +102,8 @@ public static class TypeFullNames
ALBApiAttribute,
S3EventAttribute,
DynamoDBEventAttribute,
SNSEventAttribute
SNSEventAttribute,
ScheduleEventAttribute
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.DynamoDB;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.Schedule;
using Amazon.Lambda.Annotations.SQS;
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
Expand Down Expand Up @@ -68,6 +69,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod
ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateDynamoDBEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateSnsEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateScheduleEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateAlbEvents(lambdaFunctionModel, methodLocation, diagnostics);
ValidateS3Events(lambdaFunctionModel, methodLocation, diagnostics);

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

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

return true;
}

Expand Down Expand Up @@ -526,6 +538,44 @@ private static void ValidateSnsEvents(LambdaFunctionModel lambdaFunctionModel, L
}
}

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

foreach (var att in lambdaFunctionModel.Attributes)
{
if (att.Type.FullName != TypeFullNames.ScheduleEventAttribute)
continue;

var scheduleEventAttribute = ((AttributeModel<ScheduleEventAttribute>)att).Data;
var validationErrors = scheduleEventAttribute.Validate();
validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidScheduleEventAttribute, methodLocation, errorMessage)));
}

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

// Validate return type - must be void or Task
if (!lambdaFunctionModel.LambdaMethod.ReturnsVoid && !lambdaFunctionModel.LambdaMethod.ReturnsVoidTask)
{
var errorMessage = $"When using the {nameof(ScheduleEventAttribute)}, 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 @@ -8,6 +8,7 @@
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
using Amazon.Lambda.Annotations.SNS;
using Amazon.Lambda.Annotations.Schedule;
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
using Amazon.Lambda.Annotations.S3;
using Amazon.Lambda.Annotations.SQS;
Expand Down Expand Up @@ -251,6 +252,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
eventName = ProcessSnsAttribute(lambdaFunction, snsAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
case AttributeModel<ScheduleEventAttribute> scheduleAttributeModel:
eventName = ProcessScheduleAttribute(lambdaFunction, scheduleAttributeModel.Data, currentSyncedEventProperties);
currentSyncedEvents.Add(eventName);
break;
}
}

Expand Down Expand Up @@ -782,6 +787,40 @@ private string ProcessSnsAttribute(ILambdaFunctionSerializable lambdaFunction, S
return att.ResourceName;
}

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

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

// Schedule expression
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Schedule", att.Schedule);

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

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

// 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

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

/// <summary>
/// The schedule expression. Supports rate and cron expressions.
/// Examples: "rate(5 minutes)", "cron(0 12 * * ? *)"
/// </summary>
public string Schedule { get; set; }

/// <summary>
/// The CloudFormation resource name for the schedule event.
/// </summary>
public string ResourceName
{
get
{
if (IsResourceNameSet)
{
return resourceName;
}
// Generate a default resource name from the schedule expression
var sanitized = string.Join(string.Empty, (Schedule ?? string.Empty).Where(char.IsLetterOrDigit));
return sanitized.Length > 0 ? sanitized : "ScheduleEvent";
}
set => resourceName = value;
}

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

/// <summary>
/// A description for the schedule rule.
/// </summary>
public string Description { get; set; } = null;
internal bool IsDescriptionSet => Description != null;

/// <summary>
/// A JSON string to pass as input to the Lambda function.
/// </summary>
public string Input { get; set; } = null;
internal bool IsInputSet => Input != null;

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

Comment thread
GarrettBeatty marked this conversation as resolved.
/// <summary>
/// Creates an instance of the <see cref="ScheduleEventAttribute"/> class.
/// </summary>
/// <param name="schedule"><see cref="Schedule"/> property</param>
public ScheduleEventAttribute(string schedule)
{
Schedule = schedule;
}

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

if (string.IsNullOrEmpty(Schedule))
{
validationErrors.Add($"{nameof(ScheduleEventAttribute.Schedule)} must not be null or empty");
}
else if (!Schedule.StartsWith("rate(") && !Schedule.StartsWith("cron("))
{
validationErrors.Add($"{nameof(ScheduleEventAttribute.Schedule)} = {Schedule}. It must start with 'rate(' or 'cron('");
}

if (IsResourceNameSet && !_resourceNameRegex.IsMatch(ResourceName))
{
validationErrors.Add($"{nameof(ScheduleEventAttribute.ResourceName)} = {ResourceName}. It must only contain alphanumeric characters and must not be an empty string");
}

return validationErrors;
}
}
}
Loading
Loading