Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .autover/changes/schedule-input-file-path-support.json
Original file line number Diff line number Diff line change
@@ -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."
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand All @@ -59,6 +61,7 @@ public CloudFormationWriter(IFileManager fileManager, IDirectoryManager director
/// </summary>
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);
Expand Down Expand Up @@ -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
Expand All @@ -821,6 +825,40 @@ private string ProcessScheduleAttribute(ILambdaFunctionSerializable lambdaFuncti
return att.ResourceName;
}

/// <summary>
/// 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).
/// </summary>
/// <param name="input">The Input value from the attribute, which may be a JSON string or a file path.</param>
/// <returns>The resolved input value — either the file contents or the original string.</returns>
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;
}

/// <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
Expand Up @@ -51,6 +51,9 @@ public string ResourceName

/// <summary>
/// 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"
/// </summary>
public string Input { get; set; } = null;
internal bool IsInputSet => Input != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ScheduleEventAttribute> { 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<string>($"{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<ScheduleEventAttribute> { 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<string>($"{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<ScheduleEventAttribute> { 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<string>($"{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<ScheduleEventAttribute> { 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<string>($"{eventPropertiesPath}.Input"));
}
}
}
Loading