mirror of
https://github.com/correl/openapi-core.git
synced 2024-12-29 11:09:25 +00:00
Django OpenAPI request/response factories
This commit is contained in:
parent
0d62c5f374
commit
2000b1215f
10 changed files with 305 additions and 105 deletions
11
openapi_core/contrib/django/__init__.py
Normal file
11
openapi_core/contrib/django/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from openapi_core.contrib.django.requests import DjangoOpenAPIRequestFactory
|
||||||
|
from openapi_core.contrib.django.responses import DjangoOpenAPIResponseFactory
|
||||||
|
|
||||||
|
# backward compatibility
|
||||||
|
DjangoOpenAPIRequest = DjangoOpenAPIRequestFactory.create
|
||||||
|
DjangoOpenAPIResponse = DjangoOpenAPIResponseFactory.create
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'DjangoOpenAPIRequestFactory', 'DjangoOpenAPIResponseFactory',
|
||||||
|
'DjangoOpenAPIRequest', 'DjangoOpenAPIResponse',
|
||||||
|
]
|
51
openapi_core/contrib/django/requests.py
Normal file
51
openapi_core/contrib/django/requests.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
"""OpenAPI core contrib django requests module"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
from openapi_core.validation.request.datatypes import (
|
||||||
|
RequestParameters, OpenAPIRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
||||||
|
#
|
||||||
|
# Currently unsupported are :
|
||||||
|
# - nested arguments, e.g.: ^comments/(?:page-(?P<page_number>\d+)/)?$
|
||||||
|
# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$
|
||||||
|
# - multiple named parameters between a single pair of slashes
|
||||||
|
# e.g.: <page_slug>-<page_id>/edit/
|
||||||
|
#
|
||||||
|
# The regex matches everything, except a "/" until "<". Than only the name
|
||||||
|
# is exported, after which it matches ">" and everything until a "/".
|
||||||
|
PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)'
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoOpenAPIRequestFactory(object):
|
||||||
|
|
||||||
|
path_regex = re.compile(PATH_PARAMETER_PATTERN)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, request):
|
||||||
|
method = request.method.lower()
|
||||||
|
|
||||||
|
if request.resolver_match is None:
|
||||||
|
path_pattern = request.path
|
||||||
|
else:
|
||||||
|
route = cls.path_regex.sub(
|
||||||
|
r'{\1}', request.resolver_match.route)
|
||||||
|
path_pattern = '/' + route
|
||||||
|
|
||||||
|
path = request.resolver_match and request.resolver_match.kwargs or {}
|
||||||
|
parameters = RequestParameters(
|
||||||
|
path=path,
|
||||||
|
query=request.GET,
|
||||||
|
header=request.headers,
|
||||||
|
cookie=request.COOKIES,
|
||||||
|
)
|
||||||
|
return OpenAPIRequest(
|
||||||
|
host_url=request._current_scheme_host,
|
||||||
|
path=request.path,
|
||||||
|
method=method,
|
||||||
|
path_pattern=path_pattern,
|
||||||
|
parameters=parameters,
|
||||||
|
body=request.body,
|
||||||
|
mimetype=request.content_type,
|
||||||
|
)
|
14
openapi_core/contrib/django/responses.py
Normal file
14
openapi_core/contrib/django/responses.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
"""OpenAPI core contrib django responses module"""
|
||||||
|
from openapi_core.validation.response.datatypes import OpenAPIResponse
|
||||||
|
|
||||||
|
|
||||||
|
class DjangoOpenAPIResponseFactory(object):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, response):
|
||||||
|
mimetype = response["Content-Type"]
|
||||||
|
return OpenAPIResponse(
|
||||||
|
data=response.content,
|
||||||
|
status_code=response.status_code,
|
||||||
|
mimetype=mimetype,
|
||||||
|
)
|
|
@ -21,6 +21,23 @@ class RequestParameters(object):
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class OpenAPIRequest(object):
|
class OpenAPIRequest(object):
|
||||||
|
"""OpenAPI request dataclass.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
path
|
||||||
|
Requested path as string.
|
||||||
|
path_pattern
|
||||||
|
The matched url pattern.
|
||||||
|
parameters
|
||||||
|
A RequestParameters object.
|
||||||
|
body
|
||||||
|
The request body, as string.
|
||||||
|
mimetype
|
||||||
|
Like content type, but without parameters (eg, without charset,
|
||||||
|
type etc.) and always lowercase.
|
||||||
|
For example if the content type is "text/HTML; charset=utf-8"
|
||||||
|
the mimetype would be "text/html".
|
||||||
|
"""
|
||||||
|
|
||||||
host_url = attr.ib()
|
host_url = attr.ib()
|
||||||
path = attr.ib()
|
path = attr.ib()
|
||||||
|
|
|
@ -6,6 +6,16 @@ from openapi_core.validation.datatypes import BaseValidationResult
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class OpenAPIResponse(object):
|
class OpenAPIResponse(object):
|
||||||
|
"""OpenAPI request dataclass.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
data
|
||||||
|
The response body, as string.
|
||||||
|
status_code
|
||||||
|
The status code as integer.
|
||||||
|
mimetype
|
||||||
|
Lowercase content type without charset.
|
||||||
|
"""
|
||||||
|
|
||||||
data = attr.ib()
|
data = attr.ib()
|
||||||
status_code = attr.ib()
|
status_code = attr.ib()
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
"""OpenAPI core wrappers module"""
|
|
||||||
import re
|
|
||||||
|
|
||||||
from openapi_core.wrappers.base import BaseOpenAPIRequest, BaseOpenAPIResponse
|
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/http/urls/
|
|
||||||
#
|
|
||||||
# Currently unsupported are :
|
|
||||||
# - nested arguments, e.g.: ^comments/(?:page-(?P<page_number>\d+)/)?$
|
|
||||||
# - unnamed regex groups, e.g.: ^articles/([0-9]{4})/$
|
|
||||||
# - multiple named parameters between a single pair of slashes e.g.: <page_slug>-<page_id>/edit/
|
|
||||||
#
|
|
||||||
# The regex matches everything, except a "/" until "<". Than only the name is exported, after which it matches ">" and
|
|
||||||
# everything until a "/".
|
|
||||||
PATH_PARAMETER_PATTERN = r'(?:[^\/]*?)<(?:(?:.*?:))*?(\w+)>(?:[^\/]*)'
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoOpenAPIRequest(BaseOpenAPIRequest):
|
|
||||||
path_regex = re.compile(PATH_PARAMETER_PATTERN)
|
|
||||||
|
|
||||||
def __init__(self, request):
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
@property
|
|
||||||
def host_url(self):
|
|
||||||
"""
|
|
||||||
:return: The host with scheme as IRI.
|
|
||||||
"""
|
|
||||||
return self.request._current_scheme_host
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
"""
|
|
||||||
:return: Requested path as unicode.
|
|
||||||
"""
|
|
||||||
return self.request.path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def method(self):
|
|
||||||
"""
|
|
||||||
:return: The request method, in lowercase.
|
|
||||||
"""
|
|
||||||
return self.request.method.lower()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path_pattern(self):
|
|
||||||
"""
|
|
||||||
:return: The matched url pattern.
|
|
||||||
"""
|
|
||||||
return self.path_regex.sub(r'{\1}', self.request.resolver_match.route)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parameters(self):
|
|
||||||
"""
|
|
||||||
:return: A dictionary of all parameters.
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'path': self.request.resolver_match.kwargs,
|
|
||||||
'query': self.request.GET,
|
|
||||||
'header': self.request.headers,
|
|
||||||
'cookie': self.request.COOKIES,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def body(self):
|
|
||||||
"""
|
|
||||||
:return: The request body, as string.
|
|
||||||
"""
|
|
||||||
return self.request.body
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mimetype(self):
|
|
||||||
"""
|
|
||||||
:return: Like content type, but without parameters (eg, without charset, type etc.) and always lowercase.
|
|
||||||
For example if the content type is "text/HTML; charset=utf-8" the mimetype would be "text/html".
|
|
||||||
"""
|
|
||||||
return self.request.content_type
|
|
||||||
|
|
||||||
|
|
||||||
class DjangoOpenAPIResponse(BaseOpenAPIResponse):
|
|
||||||
|
|
||||||
def __init__(self, response):
|
|
||||||
self.response = response
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self):
|
|
||||||
"""
|
|
||||||
:return: The response body, as string.
|
|
||||||
"""
|
|
||||||
return self.response.content
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status_code(self):
|
|
||||||
"""
|
|
||||||
:return: The status code as integer.
|
|
||||||
"""
|
|
||||||
return self.response.status_code
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mimetype(self):
|
|
||||||
"""
|
|
||||||
:return: Lowercase content type without charset.
|
|
||||||
"""
|
|
||||||
return self.response["Content-Type"]
|
|
|
@ -2,4 +2,5 @@ mock==2.0.0
|
||||||
pytest==3.5.0
|
pytest==3.5.0
|
||||||
pytest-flake8
|
pytest-flake8
|
||||||
pytest-cov==2.5.1
|
pytest-cov==2.5.1
|
||||||
flask
|
flask
|
||||||
|
django==2.2.6
|
||||||
|
|
|
@ -43,6 +43,7 @@ exclude =
|
||||||
tests
|
tests
|
||||||
|
|
||||||
[options.extras_require]
|
[options.extras_require]
|
||||||
|
django = django
|
||||||
flask = werkzeug
|
flask = werkzeug
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
|
|
180
tests/integration/contrib/test_django.py
Normal file
180
tests/integration/contrib/test_django.py
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.urls import resolve
|
||||||
|
import pytest
|
||||||
|
from six import b
|
||||||
|
|
||||||
|
from openapi_core.contrib.django import (
|
||||||
|
DjangoOpenAPIRequest, DjangoOpenAPIResponse,
|
||||||
|
)
|
||||||
|
from openapi_core.shortcuts import create_spec
|
||||||
|
from openapi_core.validation.request.datatypes import RequestParameters
|
||||||
|
from openapi_core.validation.request.validators import RequestValidator
|
||||||
|
from openapi_core.validation.response.validators import ResponseValidator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope='module')
|
||||||
|
def django_settings():
|
||||||
|
import django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
settings.configure(
|
||||||
|
ALLOWED_HOSTS=[
|
||||||
|
'testserver',
|
||||||
|
],
|
||||||
|
INSTALLED_APPS=[
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
],
|
||||||
|
MIDDLEWARE=[
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
django.setup()
|
||||||
|
settings.ROOT_URLCONF = (
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDjangoOpenAPIRequest(object):
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def request_factory(self):
|
||||||
|
return RequestFactory()
|
||||||
|
|
||||||
|
def test_no_resolver(self, request_factory):
|
||||||
|
request = request_factory.get('/admin/')
|
||||||
|
|
||||||
|
openapi_request = DjangoOpenAPIRequest(request)
|
||||||
|
|
||||||
|
path = {}
|
||||||
|
query = {}
|
||||||
|
headers = {
|
||||||
|
'Cookie': '',
|
||||||
|
}
|
||||||
|
cookies = {}
|
||||||
|
assert openapi_request.parameters == RequestParameters(
|
||||||
|
path=path,
|
||||||
|
query=query,
|
||||||
|
header=headers,
|
||||||
|
cookie=cookies,
|
||||||
|
)
|
||||||
|
assert openapi_request.host_url == request._current_scheme_host
|
||||||
|
assert openapi_request.path == request.path
|
||||||
|
assert openapi_request.method == request.method.lower()
|
||||||
|
assert openapi_request.path_pattern == request.path
|
||||||
|
assert openapi_request.body == request.body
|
||||||
|
assert openapi_request.mimetype == request.content_type
|
||||||
|
|
||||||
|
def test_simple(self, request_factory):
|
||||||
|
request = request_factory.get('/admin/')
|
||||||
|
request.resolver_match = resolve('/admin/')
|
||||||
|
|
||||||
|
openapi_request = DjangoOpenAPIRequest(request)
|
||||||
|
|
||||||
|
path = {}
|
||||||
|
query = {}
|
||||||
|
headers = {
|
||||||
|
'Cookie': '',
|
||||||
|
}
|
||||||
|
cookies = {}
|
||||||
|
assert openapi_request.parameters == RequestParameters(
|
||||||
|
path=path,
|
||||||
|
query=query,
|
||||||
|
header=headers,
|
||||||
|
cookie=cookies,
|
||||||
|
)
|
||||||
|
assert openapi_request.host_url == request._current_scheme_host
|
||||||
|
assert openapi_request.path == request.path
|
||||||
|
assert openapi_request.method == request.method.lower()
|
||||||
|
assert openapi_request.path_pattern == request.path
|
||||||
|
assert openapi_request.body == request.body
|
||||||
|
assert openapi_request.mimetype == request.content_type
|
||||||
|
|
||||||
|
def test_url_rule(self, request_factory):
|
||||||
|
request = request_factory.get('/admin/auth/group/1/')
|
||||||
|
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||||
|
|
||||||
|
openapi_request = DjangoOpenAPIRequest(request)
|
||||||
|
|
||||||
|
path = {
|
||||||
|
'object_id': '1',
|
||||||
|
}
|
||||||
|
query = {}
|
||||||
|
headers = {
|
||||||
|
'Cookie': '',
|
||||||
|
}
|
||||||
|
cookies = {}
|
||||||
|
assert openapi_request.parameters == RequestParameters(
|
||||||
|
path=path,
|
||||||
|
query=query,
|
||||||
|
header=headers,
|
||||||
|
cookie=cookies,
|
||||||
|
)
|
||||||
|
assert openapi_request.host_url == request._current_scheme_host
|
||||||
|
assert openapi_request.path == request.path
|
||||||
|
assert openapi_request.method == request.method.lower()
|
||||||
|
assert openapi_request.path_pattern == \
|
||||||
|
"/admin/auth/group/{object_id}/"
|
||||||
|
assert openapi_request.body == request.body
|
||||||
|
assert openapi_request.mimetype == request.content_type
|
||||||
|
|
||||||
|
|
||||||
|
class TestDjangoOpenAPIResponse:
|
||||||
|
|
||||||
|
def test_stream_response(self):
|
||||||
|
response = HttpResponse()
|
||||||
|
response.writelines(['foo\n', 'bar\n', 'baz\n'])
|
||||||
|
|
||||||
|
openapi_response = DjangoOpenAPIResponse(response)
|
||||||
|
|
||||||
|
assert openapi_response.data == b('foo\nbar\nbaz\n')
|
||||||
|
assert openapi_response.status_code == response.status_code
|
||||||
|
assert openapi_response.mimetype == response["Content-Type"]
|
||||||
|
|
||||||
|
def test_redirect_response(self):
|
||||||
|
response = HttpResponseRedirect('/redirected/')
|
||||||
|
|
||||||
|
openapi_response = DjangoOpenAPIResponse(response)
|
||||||
|
|
||||||
|
assert openapi_response.data == response.content
|
||||||
|
assert openapi_response.status_code == response.status_code
|
||||||
|
assert openapi_response.mimetype == response["Content-Type"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestDjangoOpenAPIValidation(object):
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def request_factory(self):
|
||||||
|
return RequestFactory()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def django_spec(self, factory):
|
||||||
|
specfile = 'data/v3.0/django_factory.yaml'
|
||||||
|
return create_spec(factory.spec_from_file(specfile))
|
||||||
|
|
||||||
|
def test_response_validator_path_pattern(
|
||||||
|
self, django_spec, request_factory):
|
||||||
|
validator = ResponseValidator(django_spec)
|
||||||
|
request = request_factory.get('/admin/auth/group/1/')
|
||||||
|
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||||
|
openapi_request = DjangoOpenAPIRequest(request)
|
||||||
|
response = HttpResponse(b('Some item'))
|
||||||
|
openapi_response = DjangoOpenAPIResponse(response)
|
||||||
|
result = validator.validate(openapi_request, openapi_response)
|
||||||
|
assert not result.errors
|
||||||
|
|
||||||
|
def test_request_validator_path_pattern(
|
||||||
|
self, django_spec, request_factory):
|
||||||
|
validator = RequestValidator(django_spec)
|
||||||
|
request = request_factory.get('/admin/auth/group/1/')
|
||||||
|
request.resolver_match = resolve('/admin/auth/group/1/')
|
||||||
|
openapi_request = DjangoOpenAPIRequest(request)
|
||||||
|
result = validator.validate(openapi_request)
|
||||||
|
assert not result.errors
|
19
tests/integration/data/v3.0/django_factory.yaml
Normal file
19
tests/integration/data/v3.0/django_factory.yaml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
openapi: "3.0.0"
|
||||||
|
info:
|
||||||
|
title: Basic OpenAPI specification used with test_flask.TestFlaskOpenAPIIValidation
|
||||||
|
version: "0.1"
|
||||||
|
servers:
|
||||||
|
- url: 'http://testserver'
|
||||||
|
paths:
|
||||||
|
'/admin/auth/group/{object_id}/':
|
||||||
|
parameters:
|
||||||
|
- name: object_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: the ID of the resource to retrieve
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
default:
|
||||||
|
description: Return the resource.
|
Loading…
Reference in a new issue