Skip to content

Commit

Permalink
feat: Support SAM API MergeDefinitions property (#2943)
Browse files Browse the repository at this point in the history
  • Loading branch information
GavinZZ authored Feb 25, 2023
1 parent ed4b015 commit 8c755fb
Show file tree
Hide file tree
Showing 33 changed files with 3,165 additions and 35 deletions.
1 change: 1 addition & 0 deletions docs/globals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Currently, the following resources and properties are being supported:
Auth:
Name:
DefinitionUri:
MergeDefinitions:
CacheClusterEnabled:
CacheClusterSize:
Variables:
Expand Down
3 changes: 3 additions & 0 deletions samtranslator/internal/schema_source/aws_serverless_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ class EndpointConfiguration(BaseModel):

Name = Optional[PassThroughProp]
DefinitionUriType = Optional[Union[str, DefinitionUri]]
MergeDefinitions = Optional[bool]
CacheClusterEnabled = Optional[PassThroughProp]
CacheClusterSize = Optional[PassThroughProp]
Variables = Optional[PassThroughProp]
Expand All @@ -184,6 +185,7 @@ class Properties(BaseModel):
Cors: Optional[CorsType] = properties("Cors")
DefinitionBody: Optional[DictStrAny] = properties("DefinitionBody")
DefinitionUri: Optional[DefinitionUriType] = properties("DefinitionUri")
MergeDefinitions: Optional[MergeDefinitions] # TODO: update docs when live
Description: Optional[PassThroughProp] = properties("Description")
DisableExecuteApiEndpoint: Optional[PassThroughProp] = properties("DisableExecuteApiEndpoint")
Domain: Optional[Domain] = properties("Domain")
Expand All @@ -208,6 +210,7 @@ class Globals(BaseModel):
DefinitionUri: Optional[PassThroughProp] = properties("DefinitionUri")
CacheClusterEnabled: Optional[CacheClusterEnabled] = properties("CacheClusterEnabled")
CacheClusterSize: Optional[CacheClusterSize] = properties("CacheClusterSize")
MergeDefinitions: Optional[MergeDefinitions] # TODO: update docs when live
Variables: Optional[Variables] = properties("Variables")
EndpointConfiguration: Optional[PassThroughProp] = properties("EndpointConfiguration")
MethodSettings: Optional[MethodSettings] = properties("MethodSettings")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,17 @@ class RequestParameters(BaseModel):
Required: Optional[bool] = requestparameters("Required")


# TODO: docs says either str or RequestParameter but implementation is an array of str or RequestParameter
# remove this comment once updated documentation
RequestModelProperty = List[Union[str, Dict[str, RequestParameters]]]


class ApiEventProperties(BaseModel):
Auth: Optional[ApiAuth] = apieventproperties("Auth")
Method: str = apieventproperties("Method")
Path: str = apieventproperties("Path")
RequestModel: Optional[RequestModel] = apieventproperties("RequestModel")
RequestParameters: Optional[Union[str, RequestParameters]] = apieventproperties("RequestParameters")
RequestParameters: Optional[RequestModelProperty] = apieventproperties("RequestParameters")
RestApiId: Optional[Union[str, Ref]] = apieventproperties("RestApiId")


Expand Down
29 changes: 19 additions & 10 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ def __init__( # noqa: too-many-arguments
stage_name: Optional[Intrinsicable[str]],
shared_api_usage_plan: Any,
template_conditions: Any,
merge_definitions: Optional[bool] = None,
tags: Optional[Dict[str, Any]] = None,
endpoint_configuration: Optional[Dict[str, Any]] = None,
method_settings: Optional[List[Any]] = None,
Expand Down Expand Up @@ -221,6 +222,7 @@ def __init__( # noqa: too-many-arguments
self.depends_on = depends_on
self.definition_body = definition_body
self.definition_uri = definition_uri
self.merge_definitions = merge_definitions
self.name = name
self.stage_name = stage_name
self.tags = tags
Expand Down Expand Up @@ -254,6 +256,7 @@ def _construct_rest_api(self) -> ApiGatewayRestApi:
:returns: the RestApi to which this SAM Api corresponds
:rtype: model.apigateway.ApiGatewayRestApi
"""
self._validate_properties()
rest_api = ApiGatewayRestApi(self.logical_id, depends_on=self.depends_on, attributes=self.resource_attributes)
# NOTE: For backwards compatibility we need to retain BinaryMediaTypes on the CloudFormation Property
# Removing this and only setting x-amazon-apigateway-binary-media-types results in other issues.
Expand All @@ -268,16 +271,6 @@ def _construct_rest_api(self) -> ApiGatewayRestApi:
# to Regional which is the only supported config.
self._set_endpoint_configuration(rest_api, "REGIONAL")

if self.definition_uri and self.definition_body:
raise InvalidResourceException(
self.logical_id, "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both."
)

if self.open_api_version and not SwaggerEditor.safe_compare_regex_with_string(
SwaggerEditor.get_openapi_versions_supported_regex(), self.open_api_version
):
raise InvalidResourceException(self.logical_id, "The OpenApiVersion value must be of the format '3.0.0'.")

self._add_cors()
self._add_auth()
self._add_gateway_responses()
Expand Down Expand Up @@ -311,6 +304,22 @@ def _construct_rest_api(self) -> ApiGatewayRestApi:

return rest_api

def _validate_properties(self) -> None:
if self.definition_uri and self.definition_body:
raise InvalidResourceException(
self.logical_id, "Specify either 'DefinitionUri' or 'DefinitionBody' property and not both."
)

if self.definition_uri and self.merge_definitions:
raise InvalidResourceException(
self.logical_id, "Cannot set 'MergeDefinitions' to True when using `DefinitionUri`."
)

if self.open_api_version and not SwaggerEditor.safe_compare_regex_with_string(
SwaggerEditor.get_openapi_versions_supported_regex(), self.open_api_version
):
raise InvalidResourceException(self.logical_id, "The OpenApiVersion value must be of the format '3.0.0'.")

def _add_endpoint_extension(self) -> None:
"""Add disableExecuteApiEndpoint if it is set in SAM
Note:
Expand Down
41 changes: 39 additions & 2 deletions samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from samtranslator.translator import logical_id_generator
from samtranslator.translator.arn_generator import ArnGenerator
from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr
from samtranslator.utils.utils import InvalidValueType, dict_deep_get
from samtranslator.validator.value_validator import sam_expect

CONDITION = "Condition"
Expand Down Expand Up @@ -705,7 +706,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]

explicit_api = kwargs["explicit_api"]
api_id = kwargs["api_id"]
if explicit_api.get("__MANAGE_SWAGGER"):
if explicit_api.get("__MANAGE_SWAGGER") or explicit_api.get("MergeDefinitions"):
self._add_swagger_integration(explicit_api, api_id, function, intrinsics_resolver) # type: ignore[no-untyped-call]

return resources
Expand Down Expand Up @@ -757,8 +758,13 @@ def _add_swagger_integration( # type: ignore[no-untyped-def] # noqa: too-many-s
:param model.apigateway.ApiGatewayRestApi rest_api: the RestApi to which the path and method should be added.
"""
swagger_body = api.get("DefinitionBody")
merge_definitions = api.get("MergeDefinitions")
if swagger_body is None:
return
if merge_definitions:
# Use a skeleton swagger body for API event source to make sure the generated definition body
# is unaffected by the inline/customer defined DefinitionBody
swagger_body = SwaggerEditor.gen_skeleton()

partition = ArnGenerator.get_partition_name()
uri = _build_apigw_integration_uri(function, partition) # type: ignore[no-untyped-call]
Expand Down Expand Up @@ -921,7 +927,38 @@ def _add_swagger_integration( # type: ignore[no-untyped-def] # noqa: too-many-s
path=self.Path, method_name=self.Method, request_parameters=parameters
)

api["DefinitionBody"] = editor.swagger
if merge_definitions:
api["DefinitionBody"] = self._get_merged_definitions(api_id, api["DefinitionBody"], editor.swagger)
else:
api["DefinitionBody"] = editor.swagger

def _get_merged_definitions(
self, api_id: str, source_definition_body: Dict[str, Any], dest_definition_body: Dict[str, Any]
) -> Dict[str, Any]:
"""
Merge SAM generated swagger definition(dest_definition_body) into inline DefinitionBody(source_definition_body):
- for a conflicting key, use SAM generated value
- otherwise include key-value pairs from both definitions
"""
merged_definition_body = source_definition_body.copy()
source_body_paths = merged_definition_body.get("paths", {})

try:
path_method_body = dict_deep_get(source_body_paths, [self.Path, self.Method]) or {}
except InvalidValueType as e:
raise InvalidResourceException(api_id, f"Property 'DefinitionBody' is invalid: {str(e)}") from e

sam_expect(path_method_body, api_id, f"DefinitionBody.paths.{self.Path}.{self.Method}").to_be_a_map()

generated_path_method_body = dest_definition_body["paths"][self.Path][self.Method]
# this guarantees that the merged definition use SAM generated value for a conflicting key
merged_path_method_body = {**path_method_body, **generated_path_method_body}

if self.Path not in source_body_paths:
source_body_paths[self.Path] = {self.Method: merged_path_method_body}
source_body_paths[self.Path][self.Method] = merged_path_method_body

return merged_definition_body

@staticmethod
def get_rest_api_id_string(rest_api_id: Any) -> Any:
Expand Down
3 changes: 3 additions & 0 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@ class SamApi(SamResourceMacro):
"Tags": PropertyType(False, IS_DICT),
"DefinitionBody": PropertyType(False, IS_DICT),
"DefinitionUri": PropertyType(False, one_of(IS_STR, IS_DICT)),
"MergeDefinitions": Property(False, is_type(bool)),
"CacheClusterEnabled": PropertyType(False, is_type(bool)),
"CacheClusterSize": PropertyType(False, IS_STR),
"Variables": PropertyType(False, IS_DICT),
Expand Down Expand Up @@ -1174,6 +1175,7 @@ class SamApi(SamResourceMacro):
Tags: Optional[Dict[str, Any]]
DefinitionBody: Optional[Dict[str, Any]]
DefinitionUri: Optional[Intrinsicable[str]]
MergeDefinitions: Optional[bool]
CacheClusterEnabled: Optional[Intrinsicable[bool]]
CacheClusterSize: Optional[Intrinsicable[str]]
Variables: Optional[Dict[str, Any]]
Expand Down Expand Up @@ -1239,6 +1241,7 @@ def to_cloudformation(self, **kwargs): # type: ignore[no-untyped-def]
template_conditions,
tags=self.Tags,
endpoint_configuration=self.EndpointConfiguration,
merge_definitions=self.MergeDefinitions,
method_settings=self.MethodSettings,
binary_media=self.BinaryMediaTypes,
minimum_compression_size=self.MinimumCompressionSize,
Expand Down
32 changes: 23 additions & 9 deletions samtranslator/schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -196857,6 +196857,10 @@
"title": "GatewayResponses",
"type": "object"
},
"MergeDefinitions": {
"title": "Mergedefinitions",
"type": "boolean"
},
"MethodSettings": {
"allOf": [
{
Expand Down Expand Up @@ -197091,6 +197095,10 @@
"title": "GatewayResponses",
"type": "object"
},
"MergeDefinitions": {
"title": "Mergedefinitions",
"type": "boolean"
},
"MethodSettings": {
"allOf": [
{
Expand Down Expand Up @@ -197723,17 +197731,23 @@
"title": "RequestModel"
},
"RequestParameters": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/RequestParameters"
}
],
"description": "Request parameters configuration for this specific Api\\+Path\\+Method\\. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`\\. \nIf a parameter is a string and not a Function Request Parameter Object, then `Required` and `Caching` will default to false\\. \n*Type*: String \\| [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
"items": {
"anyOf": [
{
"type": "string"
},
{
"additionalProperties": {
"$ref": "#/definitions/RequestParameters"
},
"type": "object"
}
]
},
"markdownDescription": "Request parameters configuration for this specific Api\\+Path\\+Method\\. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`\\. \nIf a parameter is a string and not a Function Request Parameter Object, then `Required` and `Caching` will default to false\\. \n*Type*: String \\| [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
"title": "RequestParameters"
"title": "RequestParameters",
"type": "array"
},
"RestApiId": {
"anyOf": [
Expand Down
6 changes: 3 additions & 3 deletions samtranslator/utils/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import copy
from typing import Any, List, Optional, cast
from typing import Any, List, Optional, Union, cast


def as_array(x: Any) -> List[Any]:
Expand Down Expand Up @@ -31,15 +31,15 @@ def __init__(self, relative_path: str) -> None:
super().__init__("It should be a map")


def dict_deep_get(d: Any, path: str) -> Optional[Any]:
def dict_deep_get(d: Any, path: Union[str, List[str]]) -> Optional[Any]:
"""
Get the value deep in the dict.
If any value along the path doesn't exist, return None.
If any parent node exists but is not a dict, raise InvalidValueType.
"""
relative_path = ""
_path_nodes = path.split(".")
_path_nodes = path.split(".") if isinstance(path, str) else path
while _path_nodes:
if d is None:
return None
Expand Down
6 changes: 6 additions & 0 deletions samtranslator/validator/sam_schema/definitions/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
],
"$ref": "#definitions/AWS::Serverless::Api.S3Location"
},
"MergeDefinitions": {
"type": [
"boolean",
"intrinsic"
]
},
"Description": {
"type": [
"string",
Expand Down
32 changes: 23 additions & 9 deletions schema_source/sam.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3256,6 +3256,10 @@
"title": "GatewayResponses",
"type": "object"
},
"MergeDefinitions": {
"title": "Mergedefinitions",
"type": "boolean"
},
"MethodSettings": {
"allOf": [
{
Expand Down Expand Up @@ -3490,6 +3494,10 @@
"title": "GatewayResponses",
"type": "object"
},
"MergeDefinitions": {
"title": "Mergedefinitions",
"type": "boolean"
},
"MethodSettings": {
"allOf": [
{
Expand Down Expand Up @@ -4122,17 +4130,23 @@
"title": "RequestModel"
},
"RequestParameters": {
"anyOf": [
{
"type": "string"
},
{
"$ref": "#/definitions/RequestParameters"
}
],
"description": "Request parameters configuration for this specific Api\\+Path\\+Method\\. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`\\. \nIf a parameter is a string and not a Function Request Parameter Object, then `Required` and `Caching` will default to false\\. \n*Type*: String \\| [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
"items": {
"anyOf": [
{
"type": "string"
},
{
"additionalProperties": {
"$ref": "#/definitions/RequestParameters"
},
"type": "object"
}
]
},
"markdownDescription": "Request parameters configuration for this specific Api\\+Path\\+Method\\. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`\\. \nIf a parameter is a string and not a Function Request Parameter Object, then `Required` and `Caching` will default to false\\. \n*Type*: String \\| [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) \n*Required*: No \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.",
"title": "RequestParameters"
"title": "RequestParameters",
"type": "array"
},
"RestApiId": {
"anyOf": [
Expand Down
Loading

0 comments on commit 8c755fb

Please sign in to comment.