Skip to content

Commit e945fc6

Browse files
committed
refactor: Refactor ImplicitHttpApiPlugin and ImplicitRestApiPlugin
1 parent f1b7e6f commit e945fc6

21 files changed

+252
-315
lines changed

samtranslator/model/naming.py

-14
This file was deleted.

samtranslator/plugins/api/implicit_api_plugin.py

+119-78
Large diffs are not rendered by default.

samtranslator/plugins/api/implicit_http_api_plugin.py

+30-76
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
from typing import Any, Dict, Optional, cast
1+
from typing import Any, Dict, Optional, Type, cast
22

33
from samtranslator.model.intrinsics import make_conditional
4-
from samtranslator.model.naming import GeneratedLogicalId
54
from samtranslator.plugins.api.implicit_api_plugin import ImplicitApiPlugin
65
from samtranslator.public.open_api import OpenApiEditor
7-
from samtranslator.public.exceptions import InvalidEventException
86
from samtranslator.public.sdk.resource import SamResourceType, SamResource
97
from samtranslator.sdk.template import SamTemplate
108
from samtranslator.validator.value_validator import sam_expect
119

1210

13-
class ImplicitHttpApiPlugin(ImplicitApiPlugin):
11+
class ImplicitHttpApiPlugin(ImplicitApiPlugin[Type[OpenApiEditor]]):
1412
"""
1513
This plugin provides Implicit Http API shorthand syntax in the SAM Spec.
1614
@@ -28,26 +26,22 @@ class ImplicitHttpApiPlugin(ImplicitApiPlugin):
2826
in OpenApi. Does **not** configure the API by any means.
2927
"""
3028

31-
def __init__(self) -> None:
32-
"""
33-
Initializes the plugin
34-
"""
35-
super(ImplicitHttpApiPlugin, self).__init__(ImplicitHttpApiPlugin.__name__)
36-
37-
def _setup_api_properties(self) -> None:
38-
"""
39-
Sets up properties that are distinct to this plugin
40-
"""
41-
self.implicit_api_logical_id = GeneratedLogicalId.implicit_http_api()
42-
self.implicit_api_condition = "ServerlessHttpApiCondition"
43-
self.api_event_type = "HttpApi"
44-
self.api_type = SamResourceType.HttpApi.value
45-
self.api_id_property = "ApiId"
46-
self.editor = OpenApiEditor
47-
48-
def _process_api_events( # type: ignore[no-untyped-def]
49-
self, function, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None
50-
):
29+
API_ID_EVENT_PROPERTY = "ApiId"
30+
IMPLICIT_API_LOGICAL_ID = "ServerlessHttpApi"
31+
IMPLICIT_API_CONDITION = "ServerlessHttpApiCondition"
32+
API_EVENT_TYPE = "HttpApi"
33+
SERVERLESS_API_RESOURCE_TYPE = SamResourceType.HttpApi.value
34+
EDITOR_CLASS = OpenApiEditor
35+
36+
def _process_api_events(
37+
self,
38+
function: SamResource,
39+
api_events: Dict[str, Dict[str, Any]],
40+
template: SamTemplate,
41+
condition: Optional[str] = None,
42+
deletion_policy: Optional[str] = None,
43+
update_replace_policy: Optional[str] = None,
44+
) -> None:
5145
"""
5246
Actually process given HTTP API events. Iteratively adds the APIs to OpenApi JSON in the respective
5347
AWS::Serverless::HttpApi resource from the template
@@ -58,21 +52,16 @@ def _process_api_events( # type: ignore[no-untyped-def]
5852
:param str condition: optional; this is the condition that is on the function with the API event
5953
"""
6054

61-
for logicalId, event in api_events.items():
55+
for event_id, event in api_events.items():
6256
# api_events only contains HttpApi events
6357
event_properties = event.get("Properties", {})
6458

65-
if not isinstance(event_properties, dict):
66-
raise InvalidEventException(
67-
logicalId,
68-
"Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.",
69-
)
70-
59+
sam_expect(event_properties, event_id, "", is_sam_event=True).to_be_a_map("Properties should be a map.")
7160
if not event_properties:
72-
event["Properties"] = event_properties
61+
event["Properties"] = event_properties # We are updating its Properties
62+
7363
self._add_implicit_api_id_if_necessary(event_properties) # type: ignore[no-untyped-call]
7464

75-
api_id = self._get_api_id(event_properties) # type: ignore[no-untyped-call]
7665
path = event_properties.get("Path", "")
7766
method = event_properties.get("Method", "")
7867
# If no path and method specified, add the $default path and ANY method
@@ -81,49 +70,20 @@ def _process_api_events( # type: ignore[no-untyped-def]
8170
method = "x-amazon-apigateway-any-method"
8271
event_properties["Path"] = path
8372
event_properties["Method"] = method
84-
elif not path or not method:
85-
key = "Path" if not path else "Method"
86-
raise InvalidEventException(logicalId, "Event is missing key '{}'.".format(key))
87-
88-
if not isinstance(path, str) or not isinstance(method, str):
89-
key = "Path" if not isinstance(path, str) else "Method"
90-
raise InvalidEventException(logicalId, "Api Event must have a String specified for '{}'.".format(key))
9173

92-
# !Ref is resolved by this time. If it is not a string, we can't parse/use this Api.
93-
if api_id and not isinstance(api_id, str):
94-
raise InvalidEventException(
95-
logicalId, "Api Event's ApiId must be a string referencing an Api in the same template."
96-
)
74+
api_id, path, method = self._validate_api_event(event_id, event_properties)
75+
self._update_resource_attributes_from_api_event(
76+
api_id, path, method, condition, deletion_policy, update_replace_policy
77+
)
9778

98-
api_dict_condition = self.api_conditions.setdefault(api_id, {})
99-
method_conditions = api_dict_condition.setdefault(path, {})
100-
method_conditions[method] = condition
101-
102-
api_dict_deletion = self.api_deletion_policies.setdefault(api_id, set())
103-
api_dict_deletion.add(deletion_policy)
104-
105-
api_dict_update_replace = self.api_update_replace_policies.setdefault(api_id, set())
106-
api_dict_update_replace.add(update_replace_policy)
107-
108-
self._add_api_to_swagger(logicalId, event_properties, template) # type: ignore[no-untyped-call]
79+
self._add_api_to_swagger(event_id, event_properties, template) # type: ignore[no-untyped-call]
10980
if "RouteSettings" in event_properties:
110-
self._add_route_settings_to_api(logicalId, event_properties, template, condition)
111-
api_events[logicalId] = event
81+
self._add_route_settings_to_api(event_id, event_properties, template, condition)
82+
api_events[event_id] = event
11283

11384
# We could have made changes to the Events structure. Write it back to function
11485
function.properties["Events"].update(api_events)
11586

116-
def _add_implicit_api_id_if_necessary(self, event_properties): # type: ignore[no-untyped-def]
117-
"""
118-
Events for implicit APIs will *not* have the RestApiId property. Absence of this property means this event
119-
is associated with the AWS::Serverless::Api ImplicitAPI resource.
120-
This method solidifies this assumption by adding RestApiId property to events that don't have them.
121-
122-
:param dict event_properties: Dictionary of event properties
123-
"""
124-
if "ApiId" not in event_properties:
125-
event_properties["ApiId"] = {"Ref": self.implicit_api_logical_id}
126-
12787
def _generate_implicit_api_resource(self) -> Dict[str, Any]:
12888
"""
12989
Uses the implicit API in this file to generate an Implicit API resource
@@ -136,12 +96,6 @@ def _get_api_definition_from_editor(self, editor: OpenApiEditor) -> Dict[str, An
13696
"""
13797
return editor.openapi
13898

139-
def _get_api_resource_type_name(self) -> str:
140-
"""
141-
Returns the type of API resource
142-
"""
143-
return "AWS::Serverless::HttpApi"
144-
14599
def _add_route_settings_to_api(
146100
self, event_id: str, event_properties: Dict[str, Any], template: SamTemplate, condition: Optional[str]
147101
) -> None:
@@ -155,7 +109,7 @@ def _add_route_settings_to_api(
155109
:param string condition: Condition on this HttpApi event (if any)
156110
"""
157111

158-
api_id = self._get_api_id(event_properties) # type: ignore[no-untyped-call]
112+
api_id = self._get_api_id(event_properties)
159113
resource = cast(SamResource, template.get(api_id)) # TODO: make this not an assumption
160114

161115
path = event_properties["Path"]

samtranslator/plugins/api/implicit_rest_api_plugin.py

+29-78
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from typing import Any, Dict
1+
from typing import Any, Dict, Optional, Type
22

3-
from samtranslator.model.naming import GeneratedLogicalId
43
from samtranslator.plugins.api.implicit_api_plugin import ImplicitApiPlugin
54
from samtranslator.public.swagger import SwaggerEditor
6-
from samtranslator.public.exceptions import InvalidEventException
75
from samtranslator.public.sdk.resource import SamResourceType, SamResource
6+
from samtranslator.sdk.template import SamTemplate
7+
from samtranslator.validator.value_validator import sam_expect
88

99

10-
class ImplicitRestApiPlugin(ImplicitApiPlugin):
10+
class ImplicitRestApiPlugin(ImplicitApiPlugin[Type[SwaggerEditor]]):
1111
"""
1212
This plugin provides Implicit API shorthand syntax in the SAM Spec.
1313
https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
@@ -26,29 +26,24 @@ class ImplicitRestApiPlugin(ImplicitApiPlugin):
2626
2727
* API Event Source (In Core Translator): ONLY adds the Lambda Integration ARN to appropriate method/path
2828
in Swagger. Does **not** configure the API by any means.
29-
3029
"""
3130

32-
def __init__(self) -> None:
33-
"""
34-
Initialize the plugin
35-
"""
36-
super(ImplicitRestApiPlugin, self).__init__(ImplicitRestApiPlugin.__name__)
37-
38-
def _setup_api_properties(self) -> None:
39-
"""
40-
Sets up properties that are distinct to this plugin
41-
"""
42-
self.implicit_api_logical_id = GeneratedLogicalId.implicit_api()
43-
self.implicit_api_condition = "ServerlessRestApiCondition"
44-
self.api_event_type = "Api"
45-
self.api_type = SamResourceType.Api.value
46-
self.api_id_property = "RestApiId"
47-
self.editor = SwaggerEditor
48-
49-
def _process_api_events( # type: ignore[no-untyped-def]
50-
self, function, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None
51-
):
31+
API_ID_EVENT_PROPERTY = "RestApiId"
32+
IMPLICIT_API_LOGICAL_ID = "ServerlessRestApi"
33+
IMPLICIT_API_CONDITION = "ServerlessRestApiCondition"
34+
API_EVENT_TYPE = "Api"
35+
SERVERLESS_API_RESOURCE_TYPE = SamResourceType.Api.value
36+
EDITOR_CLASS = SwaggerEditor
37+
38+
def _process_api_events(
39+
self,
40+
function: SamResource,
41+
api_events: Dict[str, Dict[str, Any]],
42+
template: SamTemplate,
43+
condition: Optional[str] = None,
44+
deletion_policy: Optional[str] = None,
45+
update_replace_policy: Optional[str] = None,
46+
) -> None:
5247
"""
5348
Actually process given API events. Iteratively adds the APIs to Swagger JSON in the respective Serverless::Api
5449
resource from the template
@@ -59,84 +54,40 @@ def _process_api_events( # type: ignore[no-untyped-def]
5954
:param str condition: optional; this is the condition that is on the function with the API event
6055
"""
6156

62-
for logicalId, event in api_events.items():
57+
for event_id, event in api_events.items():
6358

6459
event_properties = event.get("Properties", {})
6560
if not event_properties:
6661
continue
6762

68-
if not isinstance(event_properties, dict):
69-
raise InvalidEventException(
70-
logicalId,
71-
"Event 'Properties' must be an Object. If you're using YAML, this may be an indentation issue.",
72-
)
63+
sam_expect(event_properties, event_id, "", is_sam_event=True).to_be_a_map("Properties should be a map.")
7364

7465
self._add_implicit_api_id_if_necessary(event_properties) # type: ignore[no-untyped-call]
7566

76-
api_id = self._get_api_id(event_properties) # type: ignore[no-untyped-call]
77-
try:
78-
path = event_properties["Path"]
79-
method = event_properties["Method"]
80-
except KeyError as e:
81-
raise InvalidEventException(logicalId, "Event is missing key {}.".format(e))
82-
83-
if not isinstance(path, str):
84-
raise InvalidEventException(logicalId, "Api Event must have a String specified for 'Path'.")
85-
if not isinstance(method, str):
86-
raise InvalidEventException(logicalId, "Api Event must have a String specified for 'Method'.")
87-
88-
# !Ref is resolved by this time. If it is not a string, we can't parse/use this Api.
89-
if api_id and not isinstance(api_id, str):
90-
raise InvalidEventException(
91-
logicalId, "Api Event's RestApiId must be a string referencing an Api in the same template."
92-
)
93-
94-
api_dict_condition = self.api_conditions.setdefault(api_id, {})
95-
method_conditions = api_dict_condition.setdefault(path, {})
96-
method_conditions[method] = condition
67+
api_id, path, method = self._validate_api_event(event_id, event_properties)
68+
self._update_resource_attributes_from_api_event(
69+
api_id, path, method, condition, deletion_policy, update_replace_policy
70+
)
9771

98-
api_dict_deletion = self.api_deletion_policies.setdefault(api_id, set())
99-
api_dict_deletion.add(deletion_policy)
72+
self._add_api_to_swagger(event_id, event_properties, template) # type: ignore[no-untyped-call]
10073

101-
api_dict_update_replace = self.api_update_replace_policies.setdefault(api_id, set())
102-
api_dict_update_replace.add(update_replace_policy)
103-
104-
self._add_api_to_swagger(logicalId, event_properties, template) # type: ignore[no-untyped-call]
105-
106-
api_events[logicalId] = event
74+
api_events[event_id] = event
10775

10876
# We could have made changes to the Events structure. Write it back to function
10977
function.properties["Events"].update(api_events)
11078

111-
def _add_implicit_api_id_if_necessary(self, event_properties): # type: ignore[no-untyped-def]
112-
"""
113-
Events for implicit APIs will *not* have the RestApiId property. Absence of this property means this event
114-
is associated with the Serverless::Api ImplicitAPI resource. This method solifies this assumption by adding
115-
RestApiId property to events that don't have them.
116-
117-
:param dict event_properties: Dictionary of event properties
118-
"""
119-
if "RestApiId" not in event_properties:
120-
event_properties["RestApiId"] = {"Ref": self.implicit_api_logical_id}
121-
12279
def _generate_implicit_api_resource(self) -> Dict[str, Any]:
12380
"""
12481
Uses the implicit API in this file to generate an Implicit API resource
12582
"""
12683
return ImplicitApiResource().to_dict()
12784

128-
def _get_api_definition_from_editor(self, editor): # type: ignore[no-untyped-def]
85+
def _get_api_definition_from_editor(self, editor: SwaggerEditor) -> Dict[str, Any]:
12986
"""
13087
Helper function to return the OAS definition from the editor
13188
"""
13289
return editor.swagger
13390

134-
def _get_api_resource_type_name(self) -> str:
135-
"""
136-
Returns the type of API resource
137-
"""
138-
return "AWS::Serverless::Api"
139-
14091

14192
class ImplicitApiResource(SamResource):
14293
"""

samtranslator/validator/value_validator.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,11 @@ def to_be_a_list_of(self, expected_type: ExpectedType, message: Optional[str] =
8686
).to_be_a(expected_type, message)
8787
return value
8888

89-
def to_be_a_string(self, message: Optional[str] = "") -> T:
90-
return self.to_be_a(ExpectedType.STRING, message)
89+
def to_be_a_string(self, message: Optional[str] = "") -> str:
90+
return cast(str, self.to_be_a(ExpectedType.STRING, message))
9191

92-
def to_be_an_integer(self, message: Optional[str] = "") -> T:
93-
return self.to_be_a(ExpectedType.INTEGER, message)
92+
def to_be_an_integer(self, message: Optional[str] = "") -> str:
93+
return cast(str, self.to_be_a(ExpectedType.INTEGER, message))
9494

9595

9696
sam_expect = _ResourcePropertyValueValidator

0 commit comments

Comments
 (0)