mirror of
https://github.com/LukeHagar/connexion.git
synced 2025-12-09 12:27:46 +00:00
Merge pull request #197 from rafaelcaricio/accept-nullable-values
Support nullable parameters
This commit is contained in:
@@ -6,7 +6,7 @@ import inspect
|
||||
import logging
|
||||
import six
|
||||
|
||||
from ..utils import boolean
|
||||
from ..utils import boolean, is_nullable, is_null
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,6 +38,9 @@ def make_type(value, type):
|
||||
|
||||
|
||||
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.get("collectionFormat") and query_param.get("collectionFormat") == "pipes":
|
||||
parts = value.split("|")
|
||||
@@ -92,7 +95,6 @@ def parameter_to_arg(parameters, function):
|
||||
path_param_definitions)
|
||||
|
||||
# Add body parameters
|
||||
if request_body is not None:
|
||||
if body_name not in arguments:
|
||||
logger.debug("Body parameter '%s' not in function arguments", body_name)
|
||||
else:
|
||||
|
||||
@@ -22,7 +22,7 @@ from jsonschema import draft4_format_checker, validate, Draft4Validator, Validat
|
||||
from werkzeug import FileStorage
|
||||
|
||||
from ..problem import problem
|
||||
from ..utils import boolean
|
||||
from ..utils import boolean, is_nullable, is_null
|
||||
|
||||
logger = logging.getLogger('connexion.decorators.validation')
|
||||
|
||||
@@ -84,13 +84,14 @@ def validate_type(param, value, parameter_type, parameter_name=None):
|
||||
|
||||
|
||||
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 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.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):
|
||||
"""
|
||||
@@ -117,6 +118,9 @@ class RequestBodyValidator(object):
|
||||
:type schema: dict
|
||||
:rtype: flask.Response | None
|
||||
"""
|
||||
if self.is_null_value_valid and is_null(data):
|
||||
return None
|
||||
|
||||
try:
|
||||
validate(data, self.schema, format_checker=draft4_format_checker)
|
||||
except ValidationError as exception:
|
||||
@@ -156,6 +160,9 @@ class ParameterValidator(object):
|
||||
@staticmethod
|
||||
def validate_parameter(parameter_type, value, param):
|
||||
if value is not None:
|
||||
if is_nullable(param) and is_null(value):
|
||||
return
|
||||
|
||||
try:
|
||||
converted_value = validate_type(param, value, parameter_type)
|
||||
except TypeValidationError as e:
|
||||
|
||||
@@ -25,7 +25,7 @@ from .decorators.response import ResponseValidator
|
||||
from .decorators.security import security_passthrough, verify_oauth, get_tokeninfo_url
|
||||
from .decorators.validation import RequestBodyValidator, ParameterValidator, TypeValidationError
|
||||
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')
|
||||
|
||||
@@ -286,13 +286,15 @@ class Operation(SecureOperation):
|
||||
@property
|
||||
def body_schema(self):
|
||||
"""
|
||||
`About operation parameters
|
||||
<https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#fixed-fields-4>`_
|
||||
The body schema definition for this operation.
|
||||
"""
|
||||
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.**
|
||||
|
||||
:rtype: dict
|
||||
@@ -302,9 +304,7 @@ class Operation(SecureOperation):
|
||||
raise InvalidSpecification(
|
||||
"{method} {path} There can be one 'body' parameter at most".format(**vars(self)))
|
||||
|
||||
body_parameters = body_parameters[0] if body_parameters else {}
|
||||
schema = body_parameters.get('schema') # type: dict
|
||||
return schema
|
||||
return body_parameters[0] if body_parameters else {}
|
||||
|
||||
@property
|
||||
def function(self):
|
||||
@@ -379,7 +379,8 @@ class Operation(SecureOperation):
|
||||
if self.parameters:
|
||||
yield ParameterValidator(self.parameters)
|
||||
if self.body_schema:
|
||||
yield RequestBodyValidator(self.body_schema)
|
||||
yield RequestBodyValidator(self.body_schema,
|
||||
is_nullable(self.body_definition))
|
||||
|
||||
@property
|
||||
def __response_validation_decorator(self):
|
||||
|
||||
@@ -163,3 +163,17 @@ def boolean(s):
|
||||
return False
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -89,6 +89,38 @@ supports collection formats "pipes" and "csv". The default format is "csv".
|
||||
|
||||
.. _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
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -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')
|
||||
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'
|
||||
|
||||
@@ -299,3 +299,21 @@ def test_array_in_path(names):
|
||||
|
||||
def test_global_response_definition():
|
||||
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
|
||||
|
||||
50
tests/fixtures/simple/swagger.yaml
vendored
50
tests/fixtures/simple/swagger.yaml
vendored
@@ -526,6 +526,56 @@ paths:
|
||||
items:
|
||||
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:
|
||||
new_stack:
|
||||
|
||||
Reference in New Issue
Block a user