From 14e051bd9121a7dbc4b05b089373f407ba7eca8e Mon Sep 17 00:00:00 2001 From: maxime Date: Tue, 1 Aug 2023 20:31:17 +0200 Subject: [PATCH] [doc-schema] the "/" route on a domain now returns the OpenAPI-validated Schema (not a list of schemas), the "dummy_domain" test now validates OpenAPI specs --- halfapi/half_domain.py | 50 ++++++++++++++++--- halfapi/testing/test_domain.py | 13 ++--- .../abc/alphabet/TEST_uuid/__init__.py | 15 ++++++ .../routers/abc/pinnochio/__init__.py | 3 ++ .../routers/arguments/__init__.py | 6 +++ .../routers/async_router/__init__.py | 19 +++++++ tests/dummy_domain/routers/config/__init__.py | 3 ++ tests/test_debug_routes.py | 12 +++-- 8 files changed, 100 insertions(+), 21 deletions(-) diff --git a/halfapi/half_domain.py b/halfapi/half_domain.py index f8dc2ae..9eb3699 100644 --- a/halfapi/half_domain.py +++ b/halfapi/half_domain.py @@ -12,6 +12,8 @@ from schema import SchemaError from starlette.applications import Starlette from starlette.routing import Router, Route +from starlette.schemas import SchemaGenerator +from .lib.responses import ORJSONResponse from .lib.acl import AclRoute @@ -42,15 +44,15 @@ class HalfDomain(Starlette): self.app = app self.m_domain = importlib.import_module(domain) if module is None else module - d_domain = getattr(self.m_domain, 'domain', domain) - self.name = d_domain['name'] - self.id = d_domain['id'] - self.version = d_domain['version'] - self.halfapi_version = d_domain.get('halfapi_version', __version__) - self.deps = d_domain.get('deps', tuple()) + self.d_domain = getattr(self.m_domain, 'domain', domain) + self.name = self.d_domain['name'] + self.id = self.d_domain['id'] + self.version = self.d_domain['version'] + self.halfapi_version = self.d_domain.get('halfapi_version', __version__) + self.deps = self.d_domain.get('deps', tuple()) if not router: - self.router = d_domain.get('routers', '.routers') + self.router = self.d_domain.get('routers', '.routers') else: self.router = router @@ -405,7 +407,7 @@ class HalfDomain(Starlette): Generator(HalfRoute) """ yield HalfRoute('/', - JSONRoute([ self.schema() ]), + self.schema_openapi(), [{'acl': lib_acl.public}], 'GET' ) @@ -459,3 +461,35 @@ class HalfDomain(Starlette): } schema['paths'] = self.schema_dict() return schema + + def schema_openapi(self) -> Route: + schema = SchemaGenerator( + { + 'openapi': '3.0.0', + 'info': { + 'title': self.name, + 'version': self.version, + 'x-acls': tuple(getattr(self.m_acl, 'ACLS', ())), + **({ + f'x-{key}': value + for key, value in self.d_domain.items() + }), + } + } + + ) + + async def inner(request, *args, **kwargs): + """ + description: | + Returns the current API routes description (OpenAPI v3) + as a JSON object + responses: + 200: + description: API Schema in OpenAPI v3 format + """ + return ORJSONResponse( + schema.get_schema(routes=request.app.routes)) + + return inner + diff --git a/halfapi/testing/test_domain.py b/halfapi/testing/test_domain.py index 6f702a8..7134ffd 100644 --- a/halfapi/testing/test_domain.py +++ b/halfapi/testing/test_domain.py @@ -122,14 +122,11 @@ class TestDomain(TestCase): def check_routes(self): r = self.client.request('get', '/') assert r.status_code == 200 - schemas = r.json() - assert isinstance(schemas, list) - for schema in schemas: - assert isinstance(schema, dict) - assert 'openapi' in schema - assert 'info' in schema - assert 'paths' in schema - assert 'domain' in schema + schema = r.json() + assert isinstance(schema, dict) + assert 'openapi' in schema + assert 'info' in schema + assert 'paths' in schema r = self.client.request('get', '/halfapi/acls') assert r.status_code == 200 diff --git a/tests/dummy_domain/routers/abc/alphabet/TEST_uuid/__init__.py b/tests/dummy_domain/routers/abc/alphabet/TEST_uuid/__init__.py index bf05af4..0f98918 100644 --- a/tests/dummy_domain/routers/abc/alphabet/TEST_uuid/__init__.py +++ b/tests/dummy_domain/routers/abc/alphabet/TEST_uuid/__init__.py @@ -12,6 +12,9 @@ async def get(test): """ description: returns the path parameter + responses: + 200: + description: test response """ return ORJSONResponse(str(test)) @@ -19,6 +22,9 @@ def post(test): """ description: returns the path parameter + responses: + 200: + description: test response """ return str(test) @@ -26,6 +32,9 @@ def patch(test): """ description: returns the path parameter + responses: + 200: + description: test response """ return str(test) @@ -33,6 +42,9 @@ def put(test): """ description: returns the path parameter + responses: + 200: + description: test response """ return str(test) @@ -40,5 +52,8 @@ def delete(test): """ description: returns the path parameter + responses: + 200: + description: test response """ return str(test) diff --git a/tests/dummy_domain/routers/abc/pinnochio/__init__.py b/tests/dummy_domain/routers/abc/pinnochio/__init__.py index 962e8bb..e03171a 100644 --- a/tests/dummy_domain/routers/abc/pinnochio/__init__.py +++ b/tests/dummy_domain/routers/abc/pinnochio/__init__.py @@ -6,5 +6,8 @@ def get(): """ description: Not implemented + responses: + 200: + description: test response """ raise NotImplementedError diff --git a/tests/dummy_domain/routers/arguments/__init__.py b/tests/dummy_domain/routers/arguments/__init__.py index a512a48..ef9cdb3 100644 --- a/tests/dummy_domain/routers/arguments/__init__.py +++ b/tests/dummy_domain/routers/arguments/__init__.py @@ -60,6 +60,9 @@ def get(data): """ description: returns the arguments passed in + responses: + 200: + description: test response """ logger.error('%s', data['foo']) return data @@ -68,6 +71,9 @@ def post(data): """ description: returns the arguments passed in + responses: + 200: + description: test response """ logger.error('%s', data) return data diff --git a/tests/dummy_domain/routers/async_router/__init__.py b/tests/dummy_domain/routers/async_router/__init__.py index cd9c977..9f7fada 100644 --- a/tests/dummy_domain/routers/async_router/__init__.py +++ b/tests/dummy_domain/routers/async_router/__init__.py @@ -25,24 +25,43 @@ ROUTES = { async def get_abc_alphabet_TEST(request, *args, **kwargs): """ description: Not implemented + responses: + 200: + description: test response + parameters: + - name: test + in: path + description: Test parameter in route with "ROUTES" constant + required: true + schema: + type: string """ return NotImplementedResponse() async def get_abc_pinnochio(request, *args, **kwargs): """ description: Not implemented + responses: + 200: + description: test response """ return NotImplementedResponse() async def get_config(request, *args, **kwargs): """ description: Not implemented + responses: + 200: + description: test response """ return NotImplementedResponse() async def get_arguments(request, *args, **kwargs): """ description: Liste des datatypes. + responses: + 200: + description: test response """ return ORJSONResponse({ 'foo': kwargs.get('data').get('foo'), diff --git a/tests/dummy_domain/routers/config/__init__.py b/tests/dummy_domain/routers/config/__init__.py index 3adef6b..890812b 100644 --- a/tests/dummy_domain/routers/config/__init__.py +++ b/tests/dummy_domain/routers/config/__init__.py @@ -12,6 +12,9 @@ def get(halfapi): """ description: returns the configuration of the domain + responses: + 200: + description: test response """ logger.error('%s', halfapi) # TODO: Remove in 0.7.0 diff --git a/tests/test_debug_routes.py b/tests/test_debug_routes.py index c229881..a855bf6 100644 --- a/tests/test_debug_routes.py +++ b/tests/test_debug_routes.py @@ -7,6 +7,10 @@ import json import os import sys import pprint +import openapi_spec_validator +import logging +logger = logging.getLogger() + from halfapi.lib.constants import API_SCHEMA @@ -58,8 +62,6 @@ def test_schema(application_debug): c = TestClient(application_debug) r = c.request('get', '/') - schemas = r.json() - assert isinstance(schemas, list) - for schema in schemas: - assert isinstance(schema, dict) - assert API_SCHEMA.validate(schema) + schema = r.json() + assert isinstance(schema, dict) + openapi_spec_validator.validate_spec(schema)