Skip to content

Commit

Permalink
Create MediaTypeDict class for range matching (#1603)
Browse files Browse the repository at this point in the history
  • Loading branch information
RobbeSneyders committed Nov 4, 2022
1 parent b8bdcc9 commit 9d7258c
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 11 deletions.
31 changes: 31 additions & 0 deletions connexion/datastructures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from fnmatch import fnmatch


class MediaTypeDict(dict):
"""
A dictionary where keys can be either media types or media type ranges. When fetching a
value from the dictionary, the provided key is checked against the ranges. The most specific
key is chosen as prescribed by the OpenAPI spec, with `type/*` being preferred above
`*/subtype`.
"""

def __getitem__(self, item):
# Sort keys in order of specificity
for key in sorted(self, key=lambda k: ("*" not in k, k), reverse=True):
if fnmatch(item, key):
return super().__getitem__(key)
raise super().__getitem__(item)

def get(self, item, default=None):
try:
return self[item]
except KeyError:
return default

def __contains__(self, item):
try:
self[item]
except KeyError:
return False
else:
return True
7 changes: 6 additions & 1 deletion connexion/middleware/request_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from starlette.types import ASGIApp, Receive, Scope, Send

from connexion import utils
from connexion.datastructures import MediaTypeDict
from connexion.decorators.uri_parsing import AbstractURIParser
from connexion.exceptions import UnsupportedMediaTypeProblem
from connexion.middleware.abstract import RoutedAPI, RoutedMiddleware
Expand Down Expand Up @@ -59,7 +60,11 @@ def validate_mime_type(self, mime_type: str) -> None:
:param mime_type: mime type from content type header
"""
if mime_type.lower() not in [c.lower() for c in self._operation.consumes]:
# Convert to MediaTypeDict to handle media-ranges
media_type_dict = MediaTypeDict(
[(c.lower(), None) for c in self._operation.consumes]
)
if mime_type.lower() not in media_type_dict:
raise UnsupportedMediaTypeProblem(
detail=f"Invalid Content-type ({mime_type}), "
f"expected {self._operation.consumes}"
Expand Down
4 changes: 3 additions & 1 deletion connexion/operations/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
from copy import copy, deepcopy

from connexion.datastructures import MediaTypeDict
from connexion.operations.abstract import AbstractOperation

from ..decorators.uri_parsing import OpenAPIURIParser
Expand Down Expand Up @@ -274,7 +275,8 @@ def body_definition(self, content_type: str = None) -> dict:
"this operation accepts multiple content types, using %s",
content_type,
)
res = self._request_body.get("content", {}).get(content_type, {})
content_type_dict = MediaTypeDict(self._request_body.get("content", {}))
res = content_type_dict.get(content_type, {})
return self.with_definitions(res)
return {}

Expand Down
23 changes: 14 additions & 9 deletions connexion/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from starlette.formparsers import FormParser, MultiPartParser
from starlette.types import Receive, Scope, Send

from connexion.datastructures import MediaTypeDict
from connexion.decorators.uri_parsing import AbstractURIParser
from connexion.decorators.validation import (
ParameterValidator,
Expand Down Expand Up @@ -306,13 +307,17 @@ def form_parser_cls(self):

VALIDATOR_MAP = {
"parameter": ParameterValidator,
"body": {
"application/json": JSONRequestBodyValidator,
"application/x-www-form-urlencoded": FormDataValidator,
"multipart/form-data": MultiPartFormDataValidator,
},
"response": {
"application/json": JSONResponseBodyValidator,
"text/plain": TextResponseBodyValidator,
},
"body": MediaTypeDict(
{
"*/*json": JSONRequestBodyValidator,
"application/x-www-form-urlencoded": FormDataValidator,
"multipart/form-data": MultiPartFormDataValidator,
}
),
"response": MediaTypeDict(
{
"*/*json": JSONResponseBodyValidator,
"text/plain": TextResponseBodyValidator,
}
),
}
10 changes: 10 additions & 0 deletions tests/api/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,13 @@ def test_global_response_definitions(schema_app):
app_client = schema_app.app.test_client()
resp = app_client.get("/v1.0/define_global_response")
assert json.loads(resp.data.decode("utf-8", "replace")) == ["general", "list"]


def test_media_range(schema_app):
app_client = schema_app.app.test_client()
headers = {"Content-type": "application/json"}

array_request = app_client.post(
"/v1.0/media_range", headers=headers, data=json.dumps({})
)
assert array_request.status_code == 200, array_request.text
4 changes: 4 additions & 0 deletions tests/fakeapi/hello/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ def test_global_response_definition():
return ["general", "list"], 200


def test_media_range():
return "OK"


def test_nullable_parameters(time_start):
if time_start is None:
return "it was None"
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/different_schemas/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ paths:
responses:
'200':
$ref: '#/components/responses/GeneralList'
/media_range:
post:
description: Test media range
operationId: fakeapi.hello.test_media_range
requestBody:
content:
'*/*':
schema:
type: object
responses:
'200':
description: OK
components:
responses:
GeneralList:
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/different_schemas/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ paths:
200:
$ref: '#/responses/GeneralList'

/media_range:
post:
description: Test media range
operationId: fakeapi.hello.test_media_range
consumes:
- '*/*'
parameters:
- name: body
in: body
schema:
type: object
responses:
'200':
description: OK

definitions:
new_stack:
type: object
Expand Down

0 comments on commit 9d7258c

Please sign in to comment.