Merge pull request #197 from rafaelcaricio/accept-nullable-values

Support nullable parameters
This commit is contained in:
João Santos
2016-03-31 14:10:17 +02:00
8 changed files with 172 additions and 22 deletions

View File

@@ -6,7 +6,7 @@ import inspect
import logging import logging
import six import six
from ..utils import boolean from ..utils import boolean, is_nullable, is_null
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,6 +38,9 @@ def make_type(value, type):
def get_val_from_param(value, query_param): def get_val_from_param(value, query_param):
if is_nullable(query_param) and is_null(value):
return None
if query_param["type"] == "array": # then logic is more complex if query_param["type"] == "array": # then logic is more complex
if query_param.get("collectionFormat") and query_param.get("collectionFormat") == "pipes": if query_param.get("collectionFormat") and query_param.get("collectionFormat") == "pipes":
parts = value.split("|") parts = value.split("|")
@@ -92,7 +95,6 @@ def parameter_to_arg(parameters, function):
path_param_definitions) path_param_definitions)
# Add body parameters # Add body parameters
if request_body is not None:
if body_name not in arguments: if body_name not in arguments:
logger.debug("Body parameter '%s' not in function arguments", body_name) logger.debug("Body parameter '%s' not in function arguments", body_name)
else: else:

View File

@@ -22,7 +22,7 @@ from jsonschema import draft4_format_checker, validate, Draft4Validator, Validat
from werkzeug import FileStorage from werkzeug import FileStorage
from ..problem import problem from ..problem import problem
from ..utils import boolean from ..utils import boolean, is_nullable, is_null
logger = logging.getLogger('connexion.decorators.validation') logger = logging.getLogger('connexion.decorators.validation')
@@ -84,13 +84,14 @@ def validate_type(param, value, parameter_type, parameter_name=None):
class RequestBodyValidator(object): class RequestBodyValidator(object):
def __init__(self, schema, has_default=False): def __init__(self, schema, is_null_value_valid=False):
""" """
:param schema: The schema of the request body :param schema: The schema of the request body
:param has_default: Flag to indicate if default value is present. :param is_nullable: Flag to indicate if null is accepted as valid value.
""" """
self.schema = schema self.schema = schema
self.has_default = schema.get('default', has_default) self.has_default = schema.get('default', False)
self.is_null_value_valid = is_null_value_valid
def __call__(self, function): def __call__(self, function):
""" """
@@ -117,6 +118,9 @@ class RequestBodyValidator(object):
:type schema: dict :type schema: dict
:rtype: flask.Response | None :rtype: flask.Response | None
""" """
if self.is_null_value_valid and is_null(data):
return None
try: try:
validate(data, self.schema, format_checker=draft4_format_checker) validate(data, self.schema, format_checker=draft4_format_checker)
except ValidationError as exception: except ValidationError as exception:
@@ -156,6 +160,9 @@ class ParameterValidator(object):
@staticmethod @staticmethod
def validate_parameter(parameter_type, value, param): def validate_parameter(parameter_type, value, param):
if value is not None: if value is not None:
if is_nullable(param) and is_null(value):
return
try: try:
converted_value = validate_type(param, value, parameter_type) converted_value = validate_type(param, value, parameter_type)
except TypeValidationError as e: except TypeValidationError as e:

View File

@@ -25,7 +25,7 @@ from .decorators.response import ResponseValidator
from .decorators.security import security_passthrough, verify_oauth, get_tokeninfo_url from .decorators.security import security_passthrough, verify_oauth, get_tokeninfo_url
from .decorators.validation import RequestBodyValidator, ParameterValidator, TypeValidationError from .decorators.validation import RequestBodyValidator, ParameterValidator, TypeValidationError
from .exceptions import InvalidSpecification from .exceptions import InvalidSpecification
from .utils import flaskify_endpoint, produces_json from .utils import flaskify_endpoint, produces_json, is_nullable
logger = logging.getLogger('connexion.operation') logger = logging.getLogger('connexion.operation')
@@ -286,13 +286,15 @@ class Operation(SecureOperation):
@property @property
def body_schema(self): def body_schema(self):
""" """
`About operation parameters The body schema definition for this operation.
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields-4>`_ """
return self.body_definition.get('schema')
@property
def body_definition(self):
"""
The body complete definition for this operation.
A list of parameters that are applicable for all the operations described under this path. These parameters can
be overridden at the operation level, but cannot be removed there. The list MUST NOT include duplicated
parameters. A unique parameter is defined by a combination of a name and location. The list can use the
Reference Object to link to parameters that are defined at the Swagger Object's parameters.
**There can be one "body" parameter at most.** **There can be one "body" parameter at most.**
:rtype: dict :rtype: dict
@@ -302,9 +304,7 @@ class Operation(SecureOperation):
raise InvalidSpecification( raise InvalidSpecification(
"{method} {path} There can be one 'body' parameter at most".format(**vars(self))) "{method} {path} There can be one 'body' parameter at most".format(**vars(self)))
body_parameters = body_parameters[0] if body_parameters else {} return body_parameters[0] if body_parameters else {}
schema = body_parameters.get('schema') # type: dict
return schema
@property @property
def function(self): def function(self):
@@ -379,7 +379,8 @@ class Operation(SecureOperation):
if self.parameters: if self.parameters:
yield ParameterValidator(self.parameters) yield ParameterValidator(self.parameters)
if self.body_schema: if self.body_schema:
yield RequestBodyValidator(self.body_schema) yield RequestBodyValidator(self.body_schema,
is_nullable(self.body_definition))
@property @property
def __response_validation_decorator(self): def __response_validation_decorator(self):

View File

@@ -163,3 +163,17 @@ def boolean(s):
return False return False
else: else:
raise ValueError('Invalid boolean value') raise ValueError('Invalid boolean value')
def is_nullable(param_def):
return param_def.get('x-nullable', False)
def is_null(value):
if hasattr(value, 'strip') and value.strip() in ['null', 'None']:
return True
if value is None:
return True
return False

View File

@@ -89,6 +89,38 @@ supports collection formats "pipes" and "csv". The default format is "csv".
.. _jsonschema: https://pypi.python.org/pypi/jsonschema .. _jsonschema: https://pypi.python.org/pypi/jsonschema
Nullable parameters
^^^^^^^^^^^^^^^^^^^
Sometimes your API should explicitly accept `nullable parameters`_. However
OpenAPI specification currently does `not support`_ officially a way to serve
this use case, Connexion adds the `x-nullable` vendor extension to parameter
definitions. It's usage would be:
.. code-block:: yaml
/countries/cities:
parameters:
- name: name
in: query
type: string
x-nullable: true
required: true
It is supported by Connexion in all parameter types: `body`, `query`,
`formData`, and `path`. Nullable values are the strings `null` and `None`.
.. warning:: Be careful on nullable paramenters for sensitive data where the
strings "null" or "None" can be `valid values`_.
.. note:: This extension will be removed as soon as OpenAPI/Swagger
Specification provide a official way of supporting nullable
values.
.. _`nullable parameters`: https://github.com/zalando/connexion/issues/182
.. _`not support`: https://github.com/OAI/OpenAPI-Specification/issues/229
.. _`valid values`: http://www.bbc.com/future/story/20160325-the-names-that-break-computer-systems
Header Parameters Header Parameters
----------------- -----------------

View File

@@ -214,3 +214,29 @@ def test_array_in_path(simple_app):
resp = app_client.get('/v1.0/test-array-in-path/one_item,another_item') resp = app_client.get('/v1.0/test-array-in-path/one_item,another_item')
assert json.loads(resp.data.decode()) == ["one_item", "another_item"] assert json.loads(resp.data.decode()) == ["one_item", "another_item"]
def test_nullable_parameter(simple_app):
app_client = simple_app.app.test_client()
resp = app_client.get('/v1.0/nullable-parameters?time_start=null')
assert json.loads(resp.data.decode()) == 'it was None'
resp = app_client.get('/v1.0/nullable-parameters?time_start=None')
assert json.loads(resp.data.decode()) == 'it was None'
time_start = 1010
resp = app_client.get(
'/v1.0/nullable-parameters?time_start={}'.format(time_start))
assert json.loads(resp.data.decode()) == time_start
resp = app_client.post('/v1.0/nullable-parameters', data={"post_param": 'None'})
assert json.loads(resp.data.decode()) == 'it was None'
resp = app_client.post('/v1.0/nullable-parameters', data={"post_param": 'null'})
assert json.loads(resp.data.decode()) == 'it was None'
resp = app_client.put('/v1.0/nullable-parameters', data="null")
assert json.loads(resp.data.decode()) == 'it was None'
resp = app_client.put('/v1.0/nullable-parameters', data="None")
assert json.loads(resp.data.decode()) == 'it was None'

View File

@@ -299,3 +299,21 @@ def test_array_in_path(names):
def test_global_response_definition(): def test_global_response_definition():
return ['general', 'list'], 200 return ['general', 'list'], 200
def test_nullable_parameters(time_start):
if time_start is None:
return 'it was None'
return time_start
def test_nullable_param_post(post_param):
if post_param is None:
return 'it was None'
return post_param
def test_nullable_param_put(contents):
if contents is None:
return 'it was None'
return contents

View File

@@ -526,6 +526,56 @@ paths:
items: items:
type: string type: string
/nullable-parameters:
post:
operationId: fakeapi.hello.test_nullable_param_post
produces:
- application/json
parameters:
- name: post_param
description: Just a testing parameter.
in: formData
type: number
format: int32
x-nullable: true
required: true
responses:
200:
description: OK
put:
operationId: fakeapi.hello.test_nullable_param_put
produces:
- application/json
parameters:
- name: contents
description: Just a testing parameter.
in: body
x-nullable: true
required: true
schema:
type: object
properties:
name:
type: string
responses:
200:
description: OK
get:
operationId: fakeapi.hello.test_nullable_parameters
produces:
- application/json
parameters:
- name: time_start
description: Just a testing parameter.
in: query
type: integer
format: int32
x-nullable: true
required: true
responses:
200:
description: OK
definitions: definitions:
new_stack: new_stack: