From ab0b81a71005cc9abe9bb47d8b149eb8b4f5e19a Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Mon, 9 Dec 2019 17:41:39 +0100 Subject: [PATCH] utils: try to guess type of params (#38328) And fix a bug where they did not appear in documentation. --- passerelle/utils/api.py | 31 +++++++++++++++++-- passerelle/views.py | 44 +++++++++++++------------- tests/test_generic_endpoint.py | 56 ++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 24 deletions(-) diff --git a/passerelle/utils/api.py b/passerelle/utils/api.py index 47196365..8a382a4e 100644 --- a/passerelle/utils/api.py +++ b/passerelle/utils/api.py @@ -148,6 +148,15 @@ class endpoint(object): return {} def get_params(self): + + def type_to_str(value): + if isinstance(value, bool): + return 'boolean' + elif isinstance(value, int): + return 'integer' + elif isinstance(value, float): + return 'float' + params = [] defaults = dict(zip( reversed(inspect.getargspec(self.func).args), @@ -156,10 +165,28 @@ class endpoint(object): if param == 'post_data': continue param_info = {'name': param} - if self.parameters and param in self.parameters and self.parameters[param].get('description'): - param_info['description'] = self.parameters[param].get('description') + if self.parameters and param in self.parameters: + info = self.parameters[param] + if info.get('description'): + param_info['description'] = info['description'] + if 'type' in info: + typ = info['type'] + if typ == 'int': + param_info['type'] = 'integer' + elif typ == 'bool': + param_info['type'] = 'boolean' + else: + param_info['type'] = typ + elif 'example_value' in info: + typ = type_to_str(info['example_value']) + if typ: + param_info['type'] = typ if param in defaults: param_info['optional'] = True param_info['default_value'] = defaults[param] + if 'type' not in param_info: + typ = type_to_str(defaults[param]) + if typ: + param_info['type'] = typ params.append(param_info) return params diff --git a/passerelle/views.py b/passerelle/views.py index 1e04a233..c4d9f58f 100644 --- a/passerelle/views.py +++ b/passerelle/views.py @@ -319,29 +319,29 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View): continue if not d.get(key): d[key] = other_params[key] - if self.endpoint.endpoint_info.parameters: + for parameter_info in self.endpoint.endpoint_info.get_params(): # check and convert parameter values - for parameter, parameter_info in self.endpoint.endpoint_info.parameters.items(): - if parameter not in d: - continue - if parameter_info.get('type') == 'bool': - if d[parameter].lower() in ('true', 'on'): - d[parameter] = True - elif d[parameter].lower() in ('false', 'off'): - d[parameter] = False - else: - raise InvalidParameterValue(parameter) - elif parameter_info.get('type') == 'int': - try: - d[parameter] = int(d[parameter]) - except ValueError: - raise InvalidParameterValue(parameter) - elif parameter_info.get('type') == 'float': - d[parameter] = d[parameter].replace(',', '.') - try: - d[parameter] = float(d[parameter]) - except ValueError: - raise InvalidParameterValue(parameter) + parameter = parameter_info['name'] + if parameter not in d: + continue + if parameter_info.get('type') in ('bool', 'boolean'): + if d[parameter].lower() in ('true', 'on'): + d[parameter] = True + elif d[parameter].lower() in ('false', 'off'): + d[parameter] = False + else: + raise InvalidParameterValue(parameter) + elif parameter_info.get('type') in ('int', 'integer'): + try: + d[parameter] = int(d[parameter]) + except ValueError: + raise InvalidParameterValue(parameter) + elif parameter_info.get('type') == 'float': + d[parameter] = d[parameter].replace(',', '.') + try: + d[parameter] = float(d[parameter]) + except ValueError: + raise InvalidParameterValue(parameter) if request.method == 'POST' and self.endpoint.endpoint_info.post: request_body = self.endpoint.endpoint_info.post.get('request_body', {}) diff --git a/tests/test_generic_endpoint.py b/tests/test_generic_endpoint.py index e3486531..209d1030 100644 --- a/tests/test_generic_endpoint.py +++ b/tests/test_generic_endpoint.py @@ -563,6 +563,62 @@ def test_endpoint_typed_params(app, db, monkeypatch): assert json_res['err_desc'] == 'invalid value for parameter "floating"' +def test_endpoint_params_type_detection(app, db, monkeypatch): + + @endpoint(methods=['get'], + parameters={ + 'bool_by_example': { + 'example_value': True, + }, + 'int_by_example': { + 'example_value': 1, + }, + 'float_by_example': { + 'example_value': 1.1, + }, + }) + def httpcall(obj, request, boolean=False, integer=1, floating=1.1, + bool_by_example=None, int_by_example=None, float_by_example=None): + return {'boolean': boolean, 'integer': integer, 'floating': floating, + 'bool_by_example': bool_by_example, 'int_by_example': int_by_example, + 'float_by_example': float_by_example} + + monkeypatch.setattr(StubInvoicesConnector, 'httpcall', httpcall, raising=False) + + connector = StubInvoicesConnector(slug='fake') + connector.save() + + json_res = app.get('/stub-invoices/fake/httpcall?boolean=True').json + assert json_res['boolean'] is True + json_res = app.get('/stub-invoices/fake/httpcall?bool_by_example=True').json + assert json_res['bool_by_example'] is True + + json_res = app.get('/stub-invoices/fake/httpcall?integer=2').json + assert json_res['integer'] == 2 + json_res = app.get('/stub-invoices/fake/httpcall?int_by_example=2').json + assert json_res['int_by_example'] == 2 + + json_res = app.get('/stub-invoices/fake/httpcall?floating=1.5').json + assert json_res['floating'] == 1.5 + json_res = app.get('/stub-invoices/fake/httpcall?float_by_example=1.5').json + assert json_res['float_by_example'] == 1.5 + + res = app.get('/stub-invoices/fake/') + for param in res.pyquery('ul.get-params li'): + param_details = param.getchildren() + name = next(el for el in param_details if 'param-name' in el.attrib['class']).text + typ = next(el for el in param_details if 'type' in el.attrib['class']).text + typ = typ.strip('()') + if 'bool' in name: + assert typ == 'boolean' + elif 'int' in name: + assert typ == 'integer' + elif 'float' in name: + assert typ == 'float' + else: + assert typ == 'string' + + class DummyConnectorBase(BaseResource): def get_availability_status(self): # naive get_availability_status method for testing -- 2.20.1