Skip to content

Commit

Permalink
Fix scaffold to support rest-json style API (getmoto#1291)
Browse files Browse the repository at this point in the history
* append appropriate urls when scaffolding

* make dispatch for rest-api

* fix dispatch for rest-json

* fix moto/core/response to obtain path and body parameters

* small fixes

* remove unused import

* fix get_int_param

* fix scaffold

* fix formatting of scaffold

* fix misc

* escape service to handle service w/ hyphen like iot-data

* escape service w/ hyphen

* fix regexp to extract region from url

* escape service

* fix syntax

* skip loading body to json object when request body is None
  • Loading branch information
toshitanian authored and JackDanger committed Oct 24, 2017
1 parent 20ef9db commit 56793a3
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 89 deletions.
3 changes: 2 additions & 1 deletion moto/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from moto.xray import xray_backends
from moto.batch import batch_backends


BACKENDS = {
'acm': acm_backends,
'apigateway': apigateway_backends,
Expand Down Expand Up @@ -74,7 +75,7 @@
'sts': sts_backends,
'route53': route53_backends,
'lambda': lambda_backends,
'xray': xray_backends
'xray': xray_backends,
}


Expand Down
79 changes: 76 additions & 3 deletions moto/core/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import xmltodict
from pkg_resources import resource_filename
from werkzeug.exceptions import HTTPException

import boto3
from moto.compat import OrderedDict
from moto.core.utils import camelcase_to_underscores, method_names_from_class

Expand Down Expand Up @@ -103,7 +105,8 @@ def response_template(self, source):
class BaseResponse(_TemplateEnvironmentMixin):

default_region = 'us-east-1'
region_regex = r'\.(.+?)\.amazonaws\.com'
# to extract region, use [^.]
region_regex = r'\.([^.]+?)\.amazonaws\.com'
aws_service_spec = None

@classmethod
Expand Down Expand Up @@ -151,12 +154,12 @@ def setup_class(self, request, full_url, headers):
querystring.update(headers)

querystring = _decode_dict(querystring)

self.uri = full_url
self.path = urlparse(full_url).path
self.querystring = querystring
self.method = request.method
self.region = self.get_region_from_url(request, full_url)
self.uri_match = None

self.headers = request.headers
if 'host' not in self.headers:
Expand All @@ -178,6 +181,58 @@ def _dispatch(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
return self.call_action()

def uri_to_regexp(self, uri):
"""converts uri w/ placeholder to regexp
'/cars/{carName}/drivers/{DriverName}'
-> '^/cars/.*/drivers/[^/]*$'
'/cars/{carName}/drivers/{DriverName}/drive'
-> '^/cars/.*/drivers/.*/drive$'
"""
def _convert(elem, is_last):
if not re.match('^{.*}$', elem):
return elem
name = elem.replace('{', '').replace('}', '')
if is_last:
return '(?P<%s>[^/]*)' % name
return '(?P<%s>.*)' % name

elems = uri.split('/')
num_elems = len(elems)
regexp = '^{}$'.format('/'.join([_convert(elem, (i == num_elems - 1)) for i, elem in enumerate(elems)]))
return regexp

def _get_action_from_method_and_request_uri(self, method, request_uri):
"""basically used for `rest-json` APIs
You can refer to example from link below
https://github.com/boto/botocore/blob/develop/botocore/data/iot/2015-05-28/service-2.json
"""

# service response class should have 'SERVICE_NAME' class member,
# if you want to get action from method and url
if not hasattr(self, 'SERVICE_NAME'):
return None
service = self.SERVICE_NAME
conn = boto3.client(service)

# make cache if it does not exist yet
if not hasattr(self, 'method_urls'):
self.method_urls = defaultdict(lambda: defaultdict(str))
op_names = conn._service_model.operation_names
for op_name in op_names:
op_model = conn._service_model.operation_model(op_name)
_method = op_model.http['method']
uri_regexp = self.uri_to_regexp(op_model.http['requestUri'])
self.method_urls[_method][uri_regexp] = op_model.name
regexp_and_names = self.method_urls[method]
for regexp, name in regexp_and_names.items():
match = re.match(regexp, request_uri)
self.uri_match = match
if match:
return name
return None

def _get_action(self):
action = self.querystring.get('Action', [""])[0]
if not action: # Some services use a header for the action
Expand All @@ -186,7 +241,9 @@ def _get_action(self):
'x-amz-target') or self.headers.get('X-Amz-Target')
if match:
action = match.split(".")[-1]

# get action from method and uri
if not action:
return self._get_action_from_method_and_request_uri(self.method, self.path)
return action

def call_action(self):
Expand Down Expand Up @@ -221,6 +278,22 @@ def _get_param(self, param_name, if_none=None):
val = self.querystring.get(param_name)
if val is not None:
return val[0]

# try to get json body parameter
if self.body is not None:
try:
return json.loads(self.body)[param_name]
except ValueError:
pass
except KeyError:
pass
# try to get path parameter
if self.uri_match:
try:
return self.uri_match.group(param_name)
except IndexError:
# do nothing if param is not found
pass
return if_none

def _get_int_param(self, param_name, if_none=None):
Expand Down
Loading

0 comments on commit 56793a3

Please sign in to comment.