Skip to content

Commit 7822949

Browse files
The ScheduleEvent Input property now supports file paths (#2341)
* 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. * fix test on linux
1 parent 9f1ece1 commit 7822949

5 files changed

Lines changed: 188 additions & 3 deletions

File tree

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": "Patch",
6+
"ChangelogMessages": [
7+
"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."
8+
]
9+
}
10+
]
11+
}

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.Collections.Generic;
1818
using System.ComponentModel;
19+
using System.IO;
1920
using System.Linq;
2021
using System.Reflection;
2122
using AuthorizerType = Amazon.Lambda.Annotations.SourceGenerator.Models.AuthorizerType;
@@ -45,6 +46,7 @@ public class CloudFormationWriter : IAnnotationReportWriter
4546
private readonly IDirectoryManager _directoryManager;
4647
private readonly ITemplateWriter _templateWriter;
4748
private readonly IDiagnosticReporter _diagnosticReporter;
49+
private string _projectRootDirectory;
4850

4951
public CloudFormationWriter(IFileManager fileManager, IDirectoryManager directoryManager, ITemplateWriter templateWriter, IDiagnosticReporter diagnosticReporter)
5052
{
@@ -59,6 +61,7 @@ public CloudFormationWriter(IFileManager fileManager, IDirectoryManager director
5961
/// </summary>
6062
public void ApplyReport(AnnotationReport report)
6163
{
64+
_projectRootDirectory = report.ProjectRootDirectory;
6265
var originalContent = _fileManager.ReadAllText(report.CloudFormationTemplatePath);
6366
var templateDirectory = _directoryManager.GetDirectoryName(report.CloudFormationTemplatePath);
6467
var relativeProjectUri = _directoryManager.GetRelativePath(templateDirectory, report.ProjectRootDirectory);
@@ -806,10 +809,11 @@ private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFuncti
806809
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Description", att.Description);
807810
}
808811

809-
// Input
812+
// Input - supports literal JSON strings or file paths (relative to project root or absolute)
810813
if (att.IsInputSet)
811814
{
812-
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", att.Input);
815+
var inputValue = ResolveInputValue(att.Input);
816+
SetEventProperty(syncedEventProperties, lambdaFunction.ResourceName, eventName, "Input", inputValue);
813817
}
814818

815819
// Enabled
@@ -821,6 +825,40 @@ private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFuncti
821825
return att.ResourceName;
822826
}
823827

828+
/// <summary>
829+
/// Resolves the Input value for a schedule event. If the value is a file path (relative to the project root
830+
/// or absolute) that points to an existing file, the file contents are read and returned.
831+
/// Otherwise, the original value is returned as-is (treated as a literal JSON string).
832+
/// </summary>
833+
/// <param name="input">The Input value from the attribute, which may be a JSON string or a file path.</param>
834+
/// <returns>The resolved input value — either the file contents or the original string.</returns>
835+
private string ResolveInputValue(string input)
836+
{
837+
if (string.IsNullOrEmpty(input))
838+
{
839+
return input;
840+
}
841+
842+
// Try as a path relative to the project root directory
843+
if (!string.IsNullOrEmpty(_projectRootDirectory))
844+
{
845+
var relativePath = Path.Combine(_projectRootDirectory, input);
846+
if (_fileManager.Exists(relativePath))
847+
{
848+
return _fileManager.ReadAllText(relativePath);
849+
}
850+
}
851+
852+
// Try as an absolute path
853+
if (Path.IsPathRooted(input) && _fileManager.Exists(input))
854+
{
855+
return _fileManager.ReadAllText(input);
856+
}
857+
858+
// Not a file path — return as-is (literal JSON string)
859+
return input;
860+
}
861+
824862
/// <summary>
825863
/// Writes all properties associated with <see cref="S3EventAttribute"/> to the serverless template.
826864
/// </summary>

Libraries/src/Amazon.Lambda.Annotations/Schedule/ScheduleEventAttribute.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public string ResourceName
5151

5252
/// <summary>
5353
/// A JSON string to pass as input to the Lambda function.
54+
/// This can also be a file path (relative to the project root or absolute) pointing to a JSON file.
55+
/// If the value resolves to an existing file, its contents will be read and used as the input.
56+
/// Examples: "{\"key\": \"value\"}", "./schedule-input.json", "C:\config\input.json"
5457
/// </summary>
5558
public string Input { get; set; } = null;
5659
internal bool IsInputSet => Input != null;

Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/InMemoryFileManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public string ReadAllText(string path)
2020

2121
public void WriteAllText(string path, string contents) => _cacheContent[path] = contents;
2222

23-
public bool Exists(string path) => throw new System.NotImplementedException();
23+
public bool Exists(string path) => _cacheContent.ContainsKey(path);
2424

2525
public FileStream Create(string path) => throw new System.NotImplementedException();
2626
}

Libraries/test/Amazon.Lambda.Annotations.SourceGenerators.Tests/WriterTests/ScheduleEventsTests.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
using Amazon.Lambda.Annotations.SourceGenerator;
5+
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
56
using Amazon.Lambda.Annotations.SourceGenerator.Models;
67
using Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes;
78
using Amazon.Lambda.Annotations.SourceGenerator.Writers;
89
using Amazon.Lambda.Annotations.Schedule;
910
using System.Collections.Generic;
11+
using System.IO;
1012
using Xunit;
1113

1214
namespace Amazon.Lambda.Annotations.SourceGenerators.Tests.WriterTests
@@ -140,5 +142,136 @@ public void VerifyScheduleEvent_MinimalAttributes(CloudFormationTemplateFormat t
140142
Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Input"));
141143
Assert.False(templateWriter.Exists($"{eventPropertiesPath}.Enabled"));
142144
}
145+
146+
[Theory]
147+
[InlineData(CloudFormationTemplateFormat.Json)]
148+
[InlineData(CloudFormationTemplateFormat.Yaml)]
149+
public void VerifyScheduleEventInput_RelativeFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat)
150+
{
151+
// ARRANGE - Set up a mock file manager with a JSON file at a relative path
152+
var mockFileManager = GetMockFileManager(string.Empty);
153+
var expectedJson = "{\"action\": \"cleanup\", \"target\": \"logs\"}";
154+
var inputFilePath = Path.Combine(ProjectRootDirectory, "schedule-input.json");
155+
mockFileManager.WriteAllText(inputFilePath, expectedJson);
156+
157+
var lambdaFunctionModel = GetLambdaFunctionModel();
158+
lambdaFunctionModel.PackageType = LambdaPackageType.Zip;
159+
160+
var att = new ScheduleEventAttribute("rate(1 hour)")
161+
{
162+
ResourceName = "HourlyCleanup",
163+
Input = "schedule-input.json"
164+
};
165+
lambdaFunctionModel.Attributes.Add(new AttributeModel<ScheduleEventAttribute> { Data = att });
166+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
167+
var report = GetAnnotationReport([lambdaFunctionModel]);
168+
169+
// ACT
170+
cloudFormationWriter.ApplyReport(report);
171+
172+
// ASSERT - The file contents should be used instead of the file path
173+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
174+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
175+
176+
var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.HourlyCleanup.Properties";
177+
Assert.Equal(expectedJson, templateWriter.GetToken<string>($"{eventPropertiesPath}.Input"));
178+
}
179+
180+
[Theory]
181+
[InlineData(CloudFormationTemplateFormat.Json)]
182+
[InlineData(CloudFormationTemplateFormat.Yaml)]
183+
public void VerifyScheduleEventInput_AbsoluteFilePath_ReadsFileContents(CloudFormationTemplateFormat templateFormat)
184+
{
185+
// ARRANGE - Set up a mock file manager with a JSON file at an absolute path
186+
// Use Path.GetTempPath() to ensure the path is rooted on both Windows and Linux
187+
var mockFileManager = GetMockFileManager(string.Empty);
188+
var expectedJson = "{\"environment\": \"production\"}";
189+
var absoluteInputPath = Path.Combine(Path.GetTempPath(), "config", "schedule-input.json");
190+
mockFileManager.WriteAllText(absoluteInputPath, expectedJson);
191+
192+
var lambdaFunctionModel = GetLambdaFunctionModel();
193+
lambdaFunctionModel.PackageType = LambdaPackageType.Zip;
194+
195+
var att = new ScheduleEventAttribute("rate(5 minutes)")
196+
{
197+
ResourceName = "FrequentCheck",
198+
Input = absoluteInputPath
199+
};
200+
lambdaFunctionModel.Attributes.Add(new AttributeModel<ScheduleEventAttribute> { Data = att });
201+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
202+
var report = GetAnnotationReport([lambdaFunctionModel]);
203+
204+
// ACT
205+
cloudFormationWriter.ApplyReport(report);
206+
207+
// ASSERT - The file contents should be used instead of the file path
208+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
209+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
210+
211+
var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.FrequentCheck.Properties";
212+
Assert.Equal(expectedJson, templateWriter.GetToken<string>($"{eventPropertiesPath}.Input"));
213+
}
214+
215+
[Theory]
216+
[InlineData(CloudFormationTemplateFormat.Json)]
217+
[InlineData(CloudFormationTemplateFormat.Yaml)]
218+
public void VerifyScheduleEventInput_LiteralJson_UsedAsIs(CloudFormationTemplateFormat templateFormat)
219+
{
220+
// ARRANGE - Input is a literal JSON string, not a file path
221+
var mockFileManager = GetMockFileManager(string.Empty);
222+
var lambdaFunctionModel = GetLambdaFunctionModel();
223+
lambdaFunctionModel.PackageType = LambdaPackageType.Zip;
224+
225+
var literalJson = "{\"key\": \"value\"}";
226+
var att = new ScheduleEventAttribute("rate(5 minutes)")
227+
{
228+
ResourceName = "LiteralInputSchedule",
229+
Input = literalJson
230+
};
231+
lambdaFunctionModel.Attributes.Add(new AttributeModel<ScheduleEventAttribute> { Data = att });
232+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
233+
var report = GetAnnotationReport([lambdaFunctionModel]);
234+
235+
// ACT
236+
cloudFormationWriter.ApplyReport(report);
237+
238+
// ASSERT - The literal JSON should be used as-is since it doesn't resolve to a file
239+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
240+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
241+
242+
var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.LiteralInputSchedule.Properties";
243+
Assert.Equal(literalJson, templateWriter.GetToken<string>($"{eventPropertiesPath}.Input"));
244+
}
245+
246+
[Theory]
247+
[InlineData(CloudFormationTemplateFormat.Json)]
248+
[InlineData(CloudFormationTemplateFormat.Yaml)]
249+
public void VerifyScheduleEventInput_NonExistentFilePath_UsedAsIs(CloudFormationTemplateFormat templateFormat)
250+
{
251+
// ARRANGE - Input looks like a file path but the file doesn't exist
252+
var mockFileManager = GetMockFileManager(string.Empty);
253+
var lambdaFunctionModel = GetLambdaFunctionModel();
254+
lambdaFunctionModel.PackageType = LambdaPackageType.Zip;
255+
256+
var nonExistentPath = "does-not-exist.json";
257+
var att = new ScheduleEventAttribute("rate(5 minutes)")
258+
{
259+
ResourceName = "MissingFileSchedule",
260+
Input = nonExistentPath
261+
};
262+
lambdaFunctionModel.Attributes.Add(new AttributeModel<ScheduleEventAttribute> { Data = att });
263+
var cloudFormationWriter = GetCloudFormationWriter(mockFileManager, _directoryManager, templateFormat, _diagnosticReporter);
264+
var report = GetAnnotationReport([lambdaFunctionModel]);
265+
266+
// ACT
267+
cloudFormationWriter.ApplyReport(report);
268+
269+
// ASSERT - The path string should be used as-is since the file doesn't exist
270+
ITemplateWriter templateWriter = templateFormat == CloudFormationTemplateFormat.Json ? new JsonWriter() : new YamlWriter();
271+
templateWriter.Parse(mockFileManager.ReadAllText(ServerlessTemplateFilePath));
272+
273+
var eventPropertiesPath = $"Resources.{lambdaFunctionModel.ResourceName}.Properties.Events.MissingFileSchedule.Properties";
274+
Assert.Equal(nonExistentPath, templateWriter.GetToken<string>($"{eventPropertiesPath}.Input"));
275+
}
143276
}
144277
}

0 commit comments

Comments
 (0)