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 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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
50
tests/fixtures/simple/swagger.yaml
vendored
50
tests/fixtures/simple/swagger.yaml
vendored
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user