Fix scaffold to support rest-json style API (#1291)
* 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
This commit is contained in:
parent
20ef9db885
commit
56793a3b2a
9 changed files with 159 additions and 89 deletions
|
|
@ -37,6 +37,7 @@ from moto.sts import sts_backends
|
|||
from moto.xray import xray_backends
|
||||
from moto.batch import batch_backends
|
||||
|
||||
|
||||
BACKENDS = {
|
||||
'acm': acm_backends,
|
||||
'apigateway': apigateway_backends,
|
||||
|
|
@ -74,7 +75,7 @@ BACKENDS = {
|
|||
'sts': sts_backends,
|
||||
'route53': route53_backends,
|
||||
'lambda': lambda_backends,
|
||||
'xray': xray_backends
|
||||
'xray': xray_backends,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ from six.moves.urllib.parse import parse_qs, urlparse
|
|||
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
|
||||
|
||||
|
|
@ -103,7 +105,8 @@ class _TemplateEnvironmentMixin(object):
|
|||
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
|
||||
|
|
@ -151,12 +154,12 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
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:
|
||||
|
|
@ -178,6 +181,58 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
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
|
||||
|
|
@ -186,7 +241,9 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
'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):
|
||||
|
|
@ -221,6 +278,22 @@ class BaseResponse(_TemplateEnvironmentMixin):
|
|||
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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue