Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Request Model Support #948

Merged
merged 16 commits into from
Jun 13, 2019
Merged
Show file tree
Hide file tree
Changes from 14 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
60 changes: 57 additions & 3 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
method_settings=None, binary_media=None, minimum_compression_size=None, cors=None,
auth=None, gateway_responses=None, access_log_setting=None, canary_setting=None,
tracing_enabled=None, resource_attributes=None, passthrough_resource_attributes=None,
open_api_version=None):
open_api_version=None, models=None):
"""Constructs an API Generator class that generates API Gateway resources

:param logical_id: Logical id of the SAM API Resource
Expand All @@ -50,6 +50,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
:param tracing_enabled: Whether active tracing with X-ray is enabled
:param resource_attributes: Resource attributes to add to API resources
:param passthrough_resource_attributes: Attributes such as `Condition` that are added to derived resources
:param models: Model definitions to be used by API methods
"""
self.logical_id = logical_id
self.cache_cluster_enabled = cache_cluster_enabled
Expand All @@ -73,6 +74,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
self.resource_attributes = resource_attributes
self.passthrough_resource_attributes = passthrough_resource_attributes
self.open_api_version = open_api_version
self.models = models

def _construct_rest_api(self):
"""Constructs and returns the ApiGateway RestApi.
Expand Down Expand Up @@ -104,6 +106,7 @@ def _construct_rest_api(self):
self._add_cors()
self._add_auth()
self._add_gateway_responses()
self._add_models()

if self.definition_uri:
rest_api.BodyS3Location = self._construct_body_s3_dict()
Expand Down Expand Up @@ -306,8 +309,9 @@ def _openapi_auth_postprocess(self, definition_body):

if self.open_api_version and re.match(SwaggerEditor.get_openapi_version_3_regex(), self.open_api_version):
if definition_body.get('securityDefinitions'):
definition_body['components'] = {}
definition_body['components']['securitySchemes'] = definition_body['securityDefinitions']
components = definition_body.get('components', {})
components['securitySchemes'] = definition_body['securityDefinitions']
definition_body['components'] = components
del definition_body['securityDefinitions']
return definition_body

Expand Down Expand Up @@ -354,6 +358,56 @@ def _add_gateway_responses(self):
# Assign the Swagger back to template
self.definition_body = swagger_editor.swagger

def _add_models(self):
"""
Add Model definitions to the Swagger file, if necessary
:return:
"""

if not self.models:
return

if self.models and not self.definition_body:
raise InvalidResourceException(self.logical_id,
"Models works only with inline Swagger specified in "
"'DefinitionBody' property")

if not SwaggerEditor.is_valid(self.definition_body):
raise InvalidResourceException(self.logical_id, "Unable to add Models definitions because "
"'DefinitionBody' does not contain a valid Swagger")

if not all(isinstance(model, dict) for model in self.models.values()):
raise InvalidResourceException(self.logical_id, "Invalid value for 'Models' property")

swagger_editor = SwaggerEditor(self.definition_body)
swagger_editor.add_models(self.models)

# Assign the Swagger back to template

self.definition_body = self._openapi_models_postprocess(swagger_editor.swagger)

def _openapi_models_postprocess(self, definition_body):
"""
Convert definitions to openapi 3 in definition body if OpenApiVersion flag is specified.

If the is swagger defined in the definition body, we treat it as a swagger spec and dod not
make any openapi 3 changes to it
"""
if definition_body.get('swagger') is not None:
return definition_body

if definition_body.get('openapi') is not None:
if self.open_api_version is None:
self.open_api_version = definition_body.get('openapi')

if self.open_api_version and re.match(SwaggerEditor.get_openapi_version_3_regex(), self.open_api_version):
if definition_body.get('definitions'):
components = definition_body.get('components', {})
components['schemas'] = definition_body['definitions']
definition_body['components'] = components
del definition_body['definitions']
return definition_body

def _get_authorizers(self, authorizers_config, default_authorizer=None):
authorizers = {}
if default_authorizer == 'AWS_IAM':
Expand Down
25 changes: 24 additions & 1 deletion samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ class Api(PushEventSource):

# Api Event sources must "always" be paired with a Serverless::Api
'RestApiId': PropertyType(True, is_str()),
'Auth': PropertyType(False, is_type(dict))
'Auth': PropertyType(False, is_type(dict)),
'RequestModel': PropertyType(False, is_type(dict))
}

def resources_to_link(self, resources):
Expand Down Expand Up @@ -564,6 +565,28 @@ def _add_swagger_integration(self, api, function):

editor.add_auth_to_method(api=api, path=self.Path, method_name=self.Method, auth=self.Auth)

if self.RequestModel:
method_model = self.RequestModel.get('Model')

if method_model:
api_models = api.get('Models')
if not api_models:
raise InvalidEventException(
self.relative_id,
'Unable to set RequestModel [{model}] on API method [{method}] for path [{path}] '
'because the related API does not define any Models.'.format(
model=method_model, method=self.Method, path=self.Path))

if not api_models.get(method_model):
raise InvalidEventException(
self.relative_id,
'Unable to set RequestModel [{model}] on API method [{method}] for path [{path}] '
'because it wasn\'t defined in the API\'s Models.'.format(
model=method_model, method=self.Method, path=self.Path))

editor.add_request_model_to_method(path=self.Path, method_name=self.Method,
request_model=self.RequestModel)

api["DefinitionBody"] = editor.swagger


Expand Down
6 changes: 4 additions & 2 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,8 @@ class SamApi(SamResourceMacro):
'AccessLogSetting': PropertyType(False, is_type(dict)),
'CanarySetting': PropertyType(False, is_type(dict)),
'TracingEnabled': PropertyType(False, is_type(bool)),
'OpenApiVersion': PropertyType(False, is_str())
'OpenApiVersion': PropertyType(False, is_str()),
'Models': PropertyType(False, is_type(dict))
}

referable_properties = {
Expand Down Expand Up @@ -485,7 +486,8 @@ def to_cloudformation(self, **kwargs):
tracing_enabled=self.TracingEnabled,
resource_attributes=self.resource_attributes,
passthrough_resource_attributes=self.get_passthrough_resource_attributes(),
open_api_version=self.OpenApiVersion)
open_api_version=self.OpenApiVersion,
models=self.Models)

rest_api, deployment, stage, permissions = api_generator.to_cloudformation()

Expand Down
81 changes: 81 additions & 0 deletions samtranslator/swagger/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(self, doc):
self.paths = self._doc["paths"]
self.security_definitions = self._doc.get("securityDefinitions", {})
self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, {})
self.definitions = self._doc.get('definitions', {})
Copy link
Contributor

Choose a reason for hiding this comment

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

To make this work with Openapi 3, you would need to move definitions under $/components/schemas.
You could add it as a post process similar to the auth components here - https://github.com/awslabs/serverless-application-model/blob/develop/samtranslator/model/api/api_generator.py#L293

Here is a link on whats changed from swagger 2.0 to openapi 3.0 -
https://blog.readme.io/an-example-filled-guide-to-swagger-3-2/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@praneetap I've updated the PR to include openapi support. Will you please have a look?

Copy link
Contributor

Choose a reason for hiding this comment

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

The changes look good to me, have you tried to deploy an openapi transformed template manually on cloud formation?
We would also really appreciate it if you add an example to the examples/.


def get_path(self, path):
path_dict = self.paths.get(path)
Expand Down Expand Up @@ -518,6 +519,61 @@ def set_method_authorizer(self, path, method_name, authorizer_name, authorizers,
elif 'AWS_IAM' not in self.security_definitions:
self.security_definitions.update(aws_iam_security_definition)

def add_request_model_to_method(self, path, method_name, request_model):
"""
Adds request model body parameter for this path/method.

:param string path: Path name
:param string method_name: Method name
:param dict request_model: Model name
"""
model_name = request_model and request_model.get('Model').lower()
model_required = request_model and request_model.get('Required')

normalized_method_name = self._normalize_method_name(method_name)
# It is possible that the method could have two definitions in a Fn::If block.
for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]):

# If no integration given, then we don't need to process this definition (could be AWS::NoValue)
if not self.method_definition_has_integration(method_definition):
continue

if self._doc.get('swagger') is not None:

existing_parameters = method_definition.get('parameters', [])

parameter = {
'in': 'body',
'name': model_name,
'schema': {
'$ref': '#/definitions/{}'.format(model_name)
}
}

if model_required is not None:
parameter['required'] = model_required

existing_parameters.append(parameter)

method_definition['parameters'] = existing_parameters

elif self._doc.get("openapi") and \
re.search(SwaggerEditor.get_openapi_version_3_regex(), self._doc["openapi"]) is not None:

method_definition['requestBody'] = {
'content': {
"application/json": {
"schema": {
"$ref": "#/components/schemas/{}".format(model_name)
}
}

}
}

if model_required is not None:
method_definition['requestBody']['required'] = model_required

def add_gateway_responses(self, gateway_responses):
"""
Add Gateway Response definitions to Swagger.
Expand All @@ -529,6 +585,29 @@ def add_gateway_responses(self, gateway_responses):
for response_type, response in gateway_responses.items():
self.gateway_responses[response_type] = response.generate_swagger()

def add_models(self, models):
"""
Add Model definitions to Swagger.

:param dict models: Dictionary of Model schemas which gets translated
:return:
"""

self.definitions = self.definitions or {}

for model_name, schema in models.items():

model_type = schema.get('type')
model_properties = schema.get('properties')

if not model_type:
raise ValueError("Invalid input. Value for type is required")

if not model_properties:
raise ValueError("Invalid input. Value for properties is required")

self.definitions[model_name.lower()] = schema

@property
def swagger(self):
"""
Expand All @@ -544,6 +623,8 @@ def swagger(self):
self._doc["securityDefinitions"] = self.security_definitions
if self.gateway_responses:
self._doc[self._X_APIGW_GATEWAY_RESPONSES] = self.gateway_responses
if self.definitions:
self._doc['definitions'] = self.definitions

return copy.deepcopy(self._doc)

Expand Down
Loading