diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs index 69c4f9428..6ce45f758 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Diagnostics/DiagnosticDescriptors.cs @@ -242,5 +242,12 @@ public static class DiagnosticDescriptors category: "AWSLambdaCSharpGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidScheduleEventAttribute = new DiagnosticDescriptor(id: "AWSLambda0132", + title: "Invalid ScheduleEventAttribute", + messageFormat: "Invalid ScheduleEventAttribute encountered: {0}", + category: "AWSLambdaCSharpGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); } } diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs index 328a29ac5..f9bf22d08 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/AttributeModelBuilder.cs @@ -1,5 +1,6 @@ using System; using Amazon.Lambda.Annotations.APIGateway; +using Amazon.Lambda.Annotations.Schedule; using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; @@ -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.ScheduleEventAttribute), SymbolEqualityComparer.Default)) + { + var data = ScheduleEventAttributeBuilder.Build(att); + model = new AttributeModel + { + 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); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs new file mode 100644 index 000000000..a7a03237d --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/Attributes/ScheduleEventAttributeBuilder.cs @@ -0,0 +1,44 @@ +using Amazon.Lambda.Annotations.Schedule; +using Microsoft.CodeAnalysis; +using System; + +namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes +{ + /// + /// Builder for . + /// + 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; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs index 3f5775851..99000d81a 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Models/EventTypeBuilder.cs @@ -26,6 +26,10 @@ public static HashSet Build(IMethodSymbol lambdaMethodSymbol, { events.Add(EventType.SQS); } + else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.ScheduleEventAttribute) + { + events.Add(EventType.Schedule); + } else if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAuthorizerAttribute || attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAuthorizerAttribute) { diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs index a5d7ce9ab..9ebf2ae67 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/SyntaxReceiver.cs @@ -21,7 +21,8 @@ internal class SyntaxReceiver : ISyntaxContextReceiver { "RestApiAuthorizerAttribute", "RestApiAuthorizer" }, { "HttpApiAttribute", "HttpApi" }, { "RestApiAttribute", "RestApi" }, - { "SQSEventAttribute", "SQSEvent" } + { "SQSEventAttribute", "SQSEvent" }, + { "ScheduleEventAttribute", "ScheduleEvent" } }; public List LambdaMethods { get; } = new List(); diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs index 6e15c2175..c88d8054a 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/TypeFullNames.cs @@ -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 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"; @@ -67,7 +70,8 @@ public static class TypeFullNames { RestApiAttribute, HttpApiAttribute, - SQSEventAttribute + SQSEventAttribute, + ScheduleEventAttribute }; } } \ No newline at end of file diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs index 733124209..edef2e12e 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Validation/LambdaFunctionValidator.cs @@ -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.Schedule; using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; using System.Collections.Generic; @@ -59,6 +60,7 @@ internal static bool ValidateFunction(GeneratorExecutionContext context, IMethod // Validate Events ValidateApiGatewayEvents(lambdaFunctionModel, methodLocation, diagnostics); ValidateSqsEvents(lambdaFunctionModel, methodLocation, diagnostics); + ValidateScheduleEvents(lambdaFunctionModel, methodLocation, diagnostics); return ReportDiagnostics(diagnosticReporter, diagnostics); } @@ -86,6 +88,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; } @@ -268,6 +280,43 @@ private static void ValidateSqsEvents(LambdaFunctionModel lambdaFunctionModel, L } } + private static void ValidateScheduleEvents(LambdaFunctionModel lambdaFunctionModel, Location methodLocation, List 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)att).Data; + var validationErrors = scheduleEventAttribute.Validate(); + validationErrors.ForEach(errorMessage => diagnostics.Add(Diagnostic.Create(DiagnosticDescriptors.InvalidScheduleEventAttribute, methodLocation, errorMessage))); + } + + // Validate method parameters + var parameters = lambdaFunctionModel.LambdaMethod.Parameters; + if (parameters.Count > 2 || + (parameters.Count >= 1 && parameters[0].Type.FullName != TypeFullNames.ScheduledEvent) || + (parameters.Count == 2 && 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 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 diagnostics) { var isValid = true; diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index a59aaf6d4..36e9ef4f5 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -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.Schedule; using Amazon.Lambda.Annotations.SQS; using Microsoft.CodeAnalysis; using System; @@ -221,6 +222,10 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la eventName = ProcessSqsAttribute(lambdaFunction, sqsAttributeModel.Data, currentSyncedEventProperties); currentSyncedEvents.Add(eventName); break; + case AttributeModel scheduleAttributeModel: + eventName = ProcessScheduleAttribute(lambdaFunction, scheduleAttributeModel.Data, currentSyncedEventProperties); + currentSyncedEvents.Add(eventName); + break; } } @@ -596,6 +601,40 @@ private string ProcessSqsAttribute(ILambdaFunctionSerializable lambdaFunction, S return att.ResourceName; } + /// + /// Writes all properties associated with to the serverless template. + /// + private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFunction, ScheduleEventAttribute att, Dictionary> 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; + } + /// /// Writes all properties associated with to the serverless template. /// diff --git a/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs b/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs new file mode 100644 index 000000000..0ef7cde51 --- /dev/null +++ b/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Amazon.Lambda.Annotations.Schedule +{ + /// + /// This attribute defines the Schedule event source configuration for a Lambda function. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class ScheduleEventAttribute : Attribute + { + private static readonly Regex _resourceNameRegex = new Regex("^[a-zA-Z0-9]+$"); + + /// + /// The schedule expression. Supports rate and cron expressions. + /// Examples: "rate(5 minutes)", "cron(0 12 * * ? *)" + /// + public string Schedule { get; set; } + + /// + /// The CloudFormation resource name for the schedule event. + /// + public string ResourceName + { + get + { + if (IsResourceNameSet) + { + return resourceName; + } + // Generate a default resource name from the schedule expression + var sanitized = string.Join(string.Empty, Schedule.Where(char.IsLetterOrDigit)); + return sanitized.Length > 0 ? sanitized : "ScheduleEvent"; + } + set => resourceName = value; + } + + private string resourceName { get; set; } = null; + internal bool IsResourceNameSet => resourceName != null; + + /// + /// A description for the schedule rule. + /// + public string Description { get; set; } = null; + internal bool IsDescriptionSet => Description != null; + + /// + /// A JSON string to pass as input to the Lambda function. + /// + public string Input { get; set; } = null; + internal bool IsInputSet => Input != null; + + /// + /// If set to false, the event source mapping will be disabled. Default value is true. + /// + public bool Enabled + { + get => enabled.GetValueOrDefault(); + set => enabled = value; + } + private bool? enabled { get; set; } + internal bool IsEnabledSet => enabled.HasValue; + + /// + /// Creates an instance of the class. + /// + /// property + public ScheduleEventAttribute(string schedule) + { + Schedule = schedule; + } + + internal List Validate() + { + var validationErrors = new List(); + + 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; + } + } +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj index b3bfb0488..95c92dafb 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Amazon.Lambda.RuntimeSupport.csproj @@ -3,7 +3,7 @@ - netstandard2.0;net6.0;net8.0;net9.0;net10.0;net11.0 + netstandard2.0;net6.0;net8.0;net9.0 1.14.2 Provides a bootstrap and Lambda Runtime API Client to help you to develop custom .NET Core Lambda Runtimes. Amazon.Lambda.RuntimeSupport diff --git a/Libraries/src/SnapshotRestore.Registry/SnapshotRestore.Registry.csproj b/Libraries/src/SnapshotRestore.Registry/SnapshotRestore.Registry.csproj index 0bb3f5886..72aa0ef6b 100644 --- a/Libraries/src/SnapshotRestore.Registry/SnapshotRestore.Registry.csproj +++ b/Libraries/src/SnapshotRestore.Registry/SnapshotRestore.Registry.csproj @@ -3,7 +3,7 @@ - net8.0;net9.0;net10.0;net11.0 + net8.0;net9.0 1.0.1 Provides a Restore Hooks library to help you register before snapshot and after restore hooks. SnapshotRestore.Registry diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs new file mode 100644 index 000000000..cb93f9929 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs @@ -0,0 +1,141 @@ +using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.Models; +using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes; +using Amazon.Lambda.Annotations.SourceGenerator.Writers; +using Amazon.Lambda.Annotations.Schedule; +using System.Collections.Generic; +using Xunit; + +namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests +{ + public partial class CloudFormationWriterTests + { + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventAttributes_AreCorrectlyApplied(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "MySchedule", + Description = "Process every 5 minutes", + Input = "{\"key\": \"value\"}", + Enabled = true + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.MySchedule"; + var eventPropertiesPath = $"{eventPath}.Properties"; + + Assert.True(templateWriter.Exists(eventPath)); + Assert.Equal("Schedule", templateWriter.GetToken($"{eventPath}.Type")); + Assert.Equal("rate(5 minutes)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("Process every 5 minutes", templateWriter.GetToken($"{eventPropertiesPath}.Description")); + Assert.Equal("{\"key\": \"value\"}", templateWriter.GetToken($"{eventPropertiesPath}.Input")); + Assert.Equal(true, templateWriter.GetToken($"{eventPropertiesPath}.Enabled")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventProperties_AreSyncedCorrectly(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + var eventResourceName = "MySchedule"; + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.{eventResourceName}.Properties"; + var syncedEventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Metadata.SyncedEventProperties"; + + var initialAttribute = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = eventResourceName, + Description = "Every 5 minutes" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = initialAttribute }]; + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + Assert.Equal("rate(5 minutes)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("Every 5 minutes", templateWriter.GetToken($"{eventPropertiesPath}.Description")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + + var syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Schedule", syncedEventProperties[eventResourceName]); + Assert.Contains("Description", syncedEventProperties[eventResourceName]); + + // Update to cron with Input + var updatedAttribute = new ScheduleEventAttribute("cron(0 12 * * ? *)") + { + ResourceName = eventResourceName, + Input = "{\"type\": \"daily\"}" + }; + lambdaFunctionModel.Attributes = [new AttributeModel { Data = updatedAttribute }]; + report = GetAnnotationReport([lambdaFunctionModel]); + cloudFormationWriter.ApplyReport(report); + + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + Assert.Equal("cron(0 12 * * ? *)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.Equal("{\"type\": \"daily\"}", templateWriter.GetToken($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Description")); + + syncedEventProperties = templateWriter.GetToken>>($"{syncedEventPropertiesPath}"); + Assert.Equal(2, syncedEventProperties[eventResourceName].Count); + Assert.Contains("Schedule", syncedEventProperties[eventResourceName]); + Assert.Contains("Input", syncedEventProperties[eventResourceName]); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEvent_MinimalAttributes(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(1 hour)") { ResourceName = "HourlySchedule" }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.HourlySchedule.Properties"; + Assert.Equal("rate(1 hour)", templateWriter.GetToken($"{eventPropertiesPath}.Schedule")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Description")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input")); + Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); + } + } +}