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
11 changes: 11 additions & 0 deletions .autover/changes/function-url-annotations-support.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"Projects": [
{
"Name": "Amazon.Lambda.Annotations",
"Type": "Minor",
"ChangelogMessages": [
"Added [FunctionUrl] attribute for configuring Lambda functions with Function URL endpoints, including optional CORS support"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

using System.Linq;
using Amazon.Lambda.Annotations.APIGateway;
using Microsoft.CodeAnalysis;

namespace Amazon.Lambda.Annotations.SourceGenerator.Models.Attributes
{
public static class FunctionUrlAttributeBuilder
{
public static FunctionUrlAttribute Build(AttributeData att)
{
var authType = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AuthType").Value.Value;

var data = new FunctionUrlAttribute
{
AuthType = authType == null ? FunctionUrlAuthType.NONE : (FunctionUrlAuthType)authType
};

var allowOrigins = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowOrigins").Value;
if (!allowOrigins.IsNull)
data.AllowOrigins = allowOrigins.Values.Select(v => v.Value as string).ToArray();

var allowMethods = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowMethods").Value;
if (!allowMethods.IsNull)
data.AllowMethods = allowMethods.Values.Select(v => v.Value as string).ToArray();

var allowHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowHeaders").Value;
if (!allowHeaders.IsNull)
data.AllowHeaders = allowHeaders.Values.Select(v => v.Value as string).ToArray();

var exposeHeaders = att.NamedArguments.FirstOrDefault(arg => arg.Key == "ExposeHeaders").Value;
if (!exposeHeaders.IsNull)
data.ExposeHeaders = exposeHeaders.Values.Select(v => v.Value as string).ToArray();

var allowCredentials = att.NamedArguments.FirstOrDefault(arg => arg.Key == "AllowCredentials").Value.Value;
if (allowCredentials != null)
data.AllowCredentials = (bool)allowCredentials;

var maxAge = att.NamedArguments.FirstOrDefault(arg => arg.Key == "MaxAge").Value.Value;
if (maxAge != null)
data.MaxAge = (int)maxAge;

return data;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// 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;
Expand All @@ -18,7 +21,8 @@ public static HashSet<EventType> Build(IMethodSymbol lambdaMethodSymbol,
foreach (var attribute in lambdaMethodSymbol.GetAttributes())
{
if (attribute.AttributeClass.ToDisplayString() == TypeFullNames.RestApiAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAttribute)
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.HttpApiAttribute
|| attribute.AttributeClass.ToDisplayString() == TypeFullNames.FunctionUrlAttribute)
{
events.Add(EventType.API);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// 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;
Expand Down Expand Up @@ -144,6 +147,14 @@ private static TypeModel BuildResponseType(IMethodSymbol lambdaMethodSymbol,
context.Compilation.GetTypeByMetadataName(TypeFullNames.ApplicationLoadBalancerResponse);
return TypeModelBuilder.Build(symbol, context);
}
else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute))
{
// Function URLs use the same payload format as HTTP API v2
var symbol = lambdaMethodModel.ReturnsVoidOrGenericTask ?
task.Construct(context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyResponse)):
context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyResponse);
return TypeModelBuilder.Build(symbol, context);
}
else
{
return lambdaMethodModel.ReturnType;
Expand Down Expand Up @@ -304,6 +315,20 @@ private static IList<ParameterModel> BuildParameters(IMethodSymbol lambdaMethodS
parameters.Add(requestParameter);
parameters.Add(contextParameter);
}
else if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute))
{
// Function URLs use the same payload format as HTTP API v2
var symbol = context.Compilation.GetTypeByMetadataName(TypeFullNames.APIGatewayHttpApiV2ProxyRequest);
var type = TypeModelBuilder.Build(symbol, context);
var requestParameter = new ParameterModel
{
Name = "__request__",
Type = type,
Documentation = "The Function URL request object that will be processed by the Lambda function handler."
};
parameters.Add(requestParameter);
parameters.Add(contextParameter);
}
else
{
// Lambda method with no event attribute are plain lambda functions, therefore, generated method will have
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
// 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 Amazon.Lambda.Annotations.SourceGenerator.FileIO;
Expand All @@ -21,6 +24,7 @@ internal class SyntaxReceiver : ISyntaxContextReceiver
{ "RestApiAuthorizerAttribute", "RestApiAuthorizer" },
{ "HttpApiAttribute", "HttpApi" },
{ "RestApiAttribute", "RestApi" },
{ "FunctionUrlAttribute", "FunctionUrl" },
{ "SQSEventAttribute", "SQSEvent" },
{ "ALBApiAttribute", "ALBApi" }
};
Expand Down Expand Up @@ -121,4 +125,4 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System.Collections.Generic;

namespace Amazon.Lambda.Annotations.SourceGenerator
Expand Down Expand Up @@ -34,6 +37,9 @@ public static class TypeFullNames
public const string FromRouteAttribute = "Amazon.Lambda.Annotations.APIGateway.FromRouteAttribute";
public const string FromCustomAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.FromCustomAuthorizerAttribute";

public const string FunctionUrlAttribute = "Amazon.Lambda.Annotations.APIGateway.FunctionUrlAttribute";
public const string FunctionUrlAuthType = "Amazon.Lambda.Annotations.APIGateway.FunctionUrlAuthType";

public const string HttpApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.HttpApiAuthorizerAttribute";
public const string RestApiAuthorizerAttribute = "Amazon.Lambda.Annotations.APIGateway.RestApiAuthorizerAttribute";

Expand Down Expand Up @@ -79,8 +85,9 @@ public static class TypeFullNames
{
RestApiAttribute,
HttpApiAttribute,
FunctionUrlAttribute,
SQSEventAttribute,
ALBApiAttribute
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Amazon.Lambda.Annotations.ALB;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.Extensions;
using Amazon.Lambda.Annotations.SourceGenerator.Models;
Expand Down Expand Up @@ -69,6 +72,7 @@ internal static bool ValidateDependencies(GeneratorExecutionContext context, IMe
{
// Check for references to "Amazon.Lambda.APIGatewayEvents" if the Lambda method is annotated with RestApi, HttpApi, or authorizer attributes.
if (lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAttribute)
|| lambdaMethodSymbol.HasAttribute(context, TypeFullNames.FunctionUrlAttribute)
|| lambdaMethodSymbol.HasAttribute(context, TypeFullNames.HttpApiAuthorizerAttribute) || lambdaMethodSymbol.HasAttribute(context, TypeFullNames.RestApiAuthorizerAttribute))
{
if (context.Compilation.ReferencedAssemblyNames.FirstOrDefault(x => x.Name == "Amazon.Lambda.APIGatewayEvents") == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Amazon.Lambda.Annotations.ALB;
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using Amazon.Lambda.Annotations.ALB;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Annotations.SourceGenerator.Diagnostics;
using Amazon.Lambda.Annotations.SourceGenerator.FileIO;
Expand Down Expand Up @@ -205,6 +208,7 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
var currentSyncedEvents = new List<string>();
var currentSyncedEventProperties = new Dictionary<string, List<string>>();
var currentAlbResources = new List<string>();
var hasFunctionUrl = false;

foreach (var attributeModel in lambdaFunction.Attributes)
{
Expand All @@ -227,6 +231,23 @@ private void ProcessLambdaFunctionEventAttributes(ILambdaFunctionSerializable la
var albResourceNames = ProcessAlbApiAttribute(lambdaFunction, albAttributeModel.Data);
currentAlbResources.AddRange(albResourceNames);
break;
case AttributeModel<FunctionUrlAttribute> functionUrlAttributeModel:
ProcessFunctionUrlAttribute(lambdaFunction, functionUrlAttributeModel.Data);
_templateWriter.SetToken($"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedFunctionUrlConfig", true);
hasFunctionUrl = true;
break;
}
}

// Remove FunctionUrlConfig only if it was previously created by Annotations (tracked via metadata).
// This preserves any manually-added FunctionUrlConfig that was not created by the source generator.
if (!hasFunctionUrl)
{
var syncedFunctionUrlConfigPath = $"Resources.{lambdaFunction.ResourceName}.Metadata.SyncedFunctionUrlConfig";
if (_templateWriter.GetToken<bool>(syncedFunctionUrlConfigPath, false))
{
_templateWriter.RemoveToken($"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig");
_templateWriter.RemoveToken(syncedFunctionUrlConfigPath);
}
}

Expand Down Expand Up @@ -297,6 +318,50 @@ private string ProcessHttpApiAttribute(ILambdaFunctionSerializable lambdaFunctio
return eventName;
}

/// <summary>
/// Writes the <see cref="FunctionUrlAttribute"/> configuration to the serverless template.
/// Unlike HttpApi/RestApi, Function URLs are configured as a property on the function resource
/// rather than as an event source.
/// </summary>
private void ProcessFunctionUrlAttribute(ILambdaFunctionSerializable lambdaFunction, FunctionUrlAttribute functionUrlAttribute)
{
var functionUrlConfigPath = $"Resources.{lambdaFunction.ResourceName}.Properties.FunctionUrlConfig";
_templateWriter.SetToken($"{functionUrlConfigPath}.AuthType", functionUrlAttribute.AuthType.ToString());

// Always remove the existing Cors block first to clear any stale properties
// from a previous generation pass, then re-emit only the currently configured values.
var corsPath = $"{functionUrlConfigPath}.Cors";
_templateWriter.RemoveToken(corsPath);

var hasCors = functionUrlAttribute.AllowOrigins != null
|| functionUrlAttribute.AllowMethods != null
|| functionUrlAttribute.AllowHeaders != null
|| functionUrlAttribute.ExposeHeaders != null
|| functionUrlAttribute.AllowCredentials
|| functionUrlAttribute.MaxAge > 0;

if (hasCors)
{
if (functionUrlAttribute.AllowOrigins != null)
_templateWriter.SetToken($"{corsPath}.AllowOrigins", new List<string>(functionUrlAttribute.AllowOrigins), TokenType.List);

if (functionUrlAttribute.AllowMethods != null)
_templateWriter.SetToken($"{corsPath}.AllowMethods", new List<string>(functionUrlAttribute.AllowMethods), TokenType.List);

if (functionUrlAttribute.AllowHeaders != null)
_templateWriter.SetToken($"{corsPath}.AllowHeaders", new List<string>(functionUrlAttribute.AllowHeaders), TokenType.List);

if (functionUrlAttribute.ExposeHeaders != null)
_templateWriter.SetToken($"{corsPath}.ExposeHeaders", new List<string>(functionUrlAttribute.ExposeHeaders), TokenType.List);

if (functionUrlAttribute.AllowCredentials)
_templateWriter.SetToken($"{corsPath}.AllowCredentials", true);

if (functionUrlAttribute.MaxAge > 0)
_templateWriter.SetToken($"{corsPath}.MaxAge", functionUrlAttribute.MaxAge);
}
}

/// <summary>
/// Processes all authorizers and writes them to the serverless template as inline authorizers within the API resources.
/// AWS SAM expects authorizers to be defined within the Auth.Authorizers property of AWS::Serverless::HttpApi or AWS::Serverless::Api resources.
Expand Down Expand Up @@ -1129,4 +1194,4 @@ private void SynchronizeEventsAndProperties(List<string> syncedEvents, Dictionar
_templateWriter.SetToken(syncedEventPropertiesPath, syncedEventProperties, TokenType.KeyVal);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

using System;

namespace Amazon.Lambda.Annotations.APIGateway
{
/// <summary>
/// Configures the Lambda function to be invoked via a Lambda Function URL.
/// </summary>
/// <remarks>
/// Function URLs use the same payload format as HTTP API v2 (APIGatewayHttpApiV2ProxyRequest/Response).
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public class FunctionUrlAttribute : Attribute
{
/// <inheritdoc cref="FunctionUrlAuthType"/>
public FunctionUrlAuthType AuthType { get; set; } = FunctionUrlAuthType.NONE;

/// <summary>
/// The allowed origins for CORS requests. Example: new[] { "https://example.com" }
/// </summary>
public string[] AllowOrigins { get; set; }

/// <summary>
/// The allowed HTTP methods for CORS requests. Example: new[] { "GET", "POST" }
/// </summary>
public string[] AllowMethods { get; set; }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be using an enum like we do in the API Gateway events. For the API Gateway we created our own enum to support "Any". Not sure if that is a valid situation here or is the absence of this property essentially "Any".


/// <summary>
/// The allowed headers for CORS requests.
/// </summary>
public string[] AllowHeaders { get; set; }

/// <summary>
/// Whether credentials are included in the CORS request.
/// </summary>
public bool AllowCredentials { get; set; }

/// <summary>
/// The expose headers for CORS responses.
/// </summary>
public string[] ExposeHeaders { get; set; }

/// <summary>
/// The maximum time in seconds that a browser can cache the CORS preflight response.
/// A value of 0 means the property is not set.
/// </summary>
public int MaxAge { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

namespace Amazon.Lambda.Annotations.APIGateway
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically function url is completely different than api gateway but uses the same api gateway v2 response type, so i kept it in the same namespace for now

{
/// <summary>
/// The type of authentication for a Lambda Function URL.
/// </summary>
public enum FunctionUrlAuthType
{
/// <summary>
/// No authentication. Anyone with the Function URL can invoke the function.
/// </summary>
NONE,

/// <summary>
/// IAM authentication. Only authenticated IAM users and roles can invoke the function.
/// </summary>
AWS_IAM
Comment on lines +14 to +19
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FunctionUrlAuthType introduces enum members in ALL_CAPS (NONE, AWS_IAM), which is inconsistent with the rest of the public enums in this package (e.g., LambdaHttpMethod.Get, HttpApiVersion.V2). Consider using PascalCase member names (e.g., None, AwsIam) and mapping to the SAM-required strings in the template writer (so public API stays idiomatic while still emitting NONE/AWS_IAM).

Suggested change
NONE,
/// <summary>
/// IAM authentication. Only authenticated IAM users and roles can invoke the function.
/// </summary>
AWS_IAM
None,
/// <summary>
/// IAM authentication. Only authenticated IAM users and roles can invoke the function.
/// </summary>
AwsIam

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it should be all caps

}
}
Loading
Loading