diff --git a/.autover/changes/schedule-input-file-path-support.json b/.autover/changes/schedule-input-file-path-support.json new file mode 100644 index 000000000..5006652a0 --- /dev/null +++ b/.autover/changes/schedule-input-file-path-support.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.Annotations", + "Type": "Patch", + "ChangelogMessages": [ + "The ScheduleEvent Input property now supports file paths (relative to the project root or absolute) in addition to literal JSON strings. If the value resolves to an existing file, its contents are read and used as the input in the CloudFormation template." + ] + } + ] +} diff --git a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs index 2419db38d..bb8e3733c 100644 --- a/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs +++ b/Libraries/src/Amazon.Lambda.Annotations.SourceGenerator/Writers/CloudFormationWriter.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Linq; using System.Reflection; using AuthorizerType = Amazon.Lambda.Annotations.SourceGenerator.Models.AuthorizerType; @@ -45,6 +46,7 @@ public class CloudFormationWriter : IAnnotationReportWriter private readonly IDirectoryManager _directoryManager; private readonly ITemplateWriter _templateWriter; private readonly IDiagnosticReporter _diagnosticReporter; + private string _projectRootDirectory; public CloudFormationWriter(IFileManager fileManager, IDirectoryManager directoryManager, ITemplateWriter templateWriter, IDiagnosticReporter diagnosticReporter) { @@ -59,6 +61,7 @@ public CloudFormationWriter(IFileManager fileManager, IDirectoryManager director /// public void ApplyReport(AnnotationReport report) { + _projectRootDirectory = report.ProjectRootDirectory; var originalContent = _fileManager.ReadAllText(report.CloudFormationTemplatePath); var templateDirectory = _directoryManager.GetDirectoryName(report.CloudFormationTemplatePath); var relativeProjectUri = _directoryManager.GetRelativePath(templateDirectory, report.ProjectRootDirectory); @@ -806,10 +809,11 @@ private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFuncti SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Description", att.Description); } - // Input + // Input - supports literal JSON strings or file paths (relative to project root or absolute) if (att.IsInputSet) { - SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", att.Input); + var inputValue = ResolveInputValue(att.Input); + SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", inputValue); } // Enabled @@ -821,6 +825,40 @@ private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFuncti return att.ResourceName; } + /// + /// Resolves the Input value for a schedule event. If the value is a file path (relative to the project root + /// or absolute) that points to an existing file, the file contents are read and returned. + /// Otherwise, the original value is returned as-is (treated as a literal JSON string). + /// + /// The Input value from the attribute, which may be a JSON string or a file path. + /// The resolved input value — either the file contents or the original string. + private string ResolveInputValue(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + // Try as a path relative to the project root directory + if (!string.IsNullOrEmpty(_projectRootDirectory)) + { + var relativePath = Path.Combine(_projectRootDirectory, input); + if (_fileManager.Exists(relativePath)) + { + return _fileManager.ReadAllText(relativePath); + } + } + + // Try as an absolute path + if (Path.IsPathRooted(input) && _fileManager.Exists(input)) + { + return _fileManager.ReadAllText(input); + } + + // Not a file path — return as-is (literal JSON string) + return input; + } + /// /// 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 index 197d3dfc5..a6b750425 100644 --- a/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs +++ b/Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs @@ -51,6 +51,9 @@ public string ResourceName /// /// A JSON string to pass as input to the Lambda function. + /// This can also be a file path (relative to the project root or absolute) pointing to a JSON file. + /// If the value resolves to an existing file, its contents will be read and used as the input. + /// Examples: "{\"key\": \"value\"}", "./schedule-input.json", "C:\config\input.json" /// public string Input { get; set; } = null; internal bool IsInputSet => Input != null; diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs index 18c550c05..b07655e36 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs @@ -20,7 +20,7 @@ public string ReadAllText(string path) public void WriteAllText(string path, string contents) => _cacheContent[path] = contents; - public bool Exists(string path) => throw new System.NotImplementedException(); + public bool Exists(string path) => _cacheContent.ContainsKey(path); public FileStream Create(string path) => throw new System.NotImplementedException(); } diff --git a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs index a9de21c87..a76a639d7 100644 --- a/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs +++ b/Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 using Amazon.Lambda.Annotations.SourceGenerator; +using Amazon.Lambda.Annotations.SourceGenerator.FileIO; 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 System.IO; using Xunit; namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests @@ -140,5 +142,136 @@ public void VerifyScheduleEvent_MinimalAttributes(CloudFormationTemplateFormat t Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input")); Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled")); } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_RelativeFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Set up a mock file manager with a JSON file at a relative path + var mockFileManager = GetMockFileManager(string.Empty); + var expectedJson = "{\"action\": \"cleanup\", \"target\": \"logs\"}"; + var inputFilePath = Path.Combine(ProjectRootDirectory, "schedule-input.json"); + mockFileManager.WriteAllText(inputFilePath, expectedJson); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(1 hour)") + { + ResourceName = "HourlyCleanup", + Input = "schedule-input.json" + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The file contents should be used instead of the file path + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.HourlyCleanup.Properties"; + Assert.Equal(expectedJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_AbsoluteFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Set up a mock file manager with a JSON file at an absolute path + // Use Path.GetTempPath() to ensure the path is rooted on both Windows and Linux + var mockFileManager = GetMockFileManager(string.Empty); + var expectedJson = "{\"environment\": \"production\"}"; + var absoluteInputPath = Path.Combine(Path.GetTempPath(), "config", "schedule-input.json"); + mockFileManager.WriteAllText(absoluteInputPath, expectedJson); + + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "FrequentCheck", + Input = absoluteInputPath + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The file contents should be used instead of the file path + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.FrequentCheck.Properties"; + Assert.Equal(expectedJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_LiteralJson_UsedAsIs(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Input is a literal JSON string, not a file path + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var literalJson = "{\"key\": \"value\"}"; + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "LiteralInputSchedule", + Input = literalJson + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The literal JSON should be used as-is since it doesn't resolve to a file + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.LiteralInputSchedule.Properties"; + Assert.Equal(literalJson, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } + + [Theory] + [InlineData(CloudFormationTemplateFormat.Json)] + [InlineData(CloudFormationTemplateFormat.Yaml)] + public void VerifyScheduleEventInput_NonExistentFilePath_UsedAsIs(CloudFormationTemplateFormat templateFormat) + { + // ARRANGE - Input looks like a file path but the file doesn't exist + var mockFileManager = GetMockFileManager(string.Empty); + var lambdaFunctionModel = GetLambdaFunctionModel(); + lambdaFunctionModel.PackageType = LambdaPackageType.Zip; + + var nonExistentPath = "does-not-exist.json"; + var att = new ScheduleEventAttribute("rate(5 minutes)") + { + ResourceName = "MissingFileSchedule", + Input = nonExistentPath + }; + lambdaFunctionModel.Attributes.Add(new AttributeModel { Data = att }); + var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter); + var report = GetAnnotationReport([lambdaFunctionModel]); + + // ACT + cloudFormationWriter.ApplyReport(report); + + // ASSERT - The path string should be used as-is since the file doesn't exist + ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter(); + templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath)); + + var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.MissingFileSchedule.Properties"; + Assert.Equal(nonExistentPath, templateWriter.GetToken($"{eventPropertiesPath}.Input")); + } } }