diff --git a/README.md b/README.md index 05304b3..aac0044 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ The reason this package exists is to give you peace of mind when providing a RES config.pyramid_openapi3_add_explorer(route='/api/v1/') ``` + The spec is also served as JSON, alongside YAML, at `route` with its extension replaced by `.json` (e.g. `/api/v1/openapi.json`). + 3. Use the `openapi` [view predicate](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#view-configuration-parameters) to enable request/response validation: ```python diff --git a/pyramid_openapi3/__init__.py b/pyramid_openapi3/__init__.py index a1b661b..8b3fc78 100644 --- a/pyramid_openapi3/__init__.py +++ b/pyramid_openapi3/__init__.py @@ -15,6 +15,7 @@ from openapi_spec_validator.readers import read_from_filename from openapi_spec_validator.versions.shortcuts import get_spec_version from pathlib import Path +from pathlib import PurePosixPath from pyramid.config import PHASE0_CONFIG from pyramid.config import PHASE1_CONFIG from pyramid.config import Configurator @@ -262,6 +263,11 @@ def add_spec_view( ) -> None: """Serve and register OpenApi 3.0 specification file. + Also serves the specification as JSON, at ``route`` with its extension + replaced by ``.json`` (e.g. ``/openapi.yaml`` -> ``/openapi.json``), under + ``route_name`` suffixed with ``_json``. Not available on + `add_spec_view_directory`. + :param filepath: absolute/relative path to the specification file :param route: URL path where to serve specification file :param route_name: Route name under which specification file will be served @@ -282,13 +288,26 @@ def register() -> None: validate(spec_dict) spec = SchemaPath.from_dict(spec_dict) + spec_json = json.dumps(spec_dict) def spec_view(request: Request) -> FileResponse: return FileResponse(filepath, request=request, content_type="text/yaml") + def spec_view_json(request: Request) -> Response: + return Response(spec_json, content_type="application/json", charset="UTF-8") + config.add_route(route_name, route) config.add_view(route_name=route_name, permission=permission, view=spec_view) + route_name_json = f"{route_name}_json" + # PurePosixPath (not Path) so this always uses forward slashes for the + # URL path, regardless of the host OS this runs on. + route_json = str(PurePosixPath(route).with_suffix(".json")) + config.add_route(route_name_json, route_json) + config.add_view( + route_name=route_name_json, permission=permission, view=spec_view_json + ) + config.registry.settings[apiname] = _create_api_settings( config, filepath, route_name, spec ) diff --git a/pyramid_openapi3/tests/test_routes.py b/pyramid_openapi3/tests/test_routes.py index 95037c9..4f15fbc 100644 --- a/pyramid_openapi3/tests/test_routes.py +++ b/pyramid_openapi3/tests/test_routes.py @@ -46,6 +46,7 @@ def test_register_routes_simple() -> None: ] assert routes == [ ("pyramid_openapi3.spec", "/openapi.yaml"), + ("pyramid_openapi3.spec_json", "/openapi.json"), ("foo", "/foo"), ("bar", "/bar"), ] @@ -92,6 +93,7 @@ def test_register_routes_with_factory() -> None: ] assert routes == [ ("pyramid_openapi3.spec", "/openapi.yaml", None), + ("pyramid_openapi3.spec_json", "/openapi.json", None), ("foo", "/foo", None), ("bar", "/bar", dummy_factory), ] @@ -128,5 +130,6 @@ def test_register_routes_with_prefix() -> None: ] assert routes == [ ("pyramid_openapi3.spec", "/openapi.yaml"), + ("pyramid_openapi3.spec_json", "/openapi.json"), ("foo", "/api/v1/foo"), ] diff --git a/pyramid_openapi3/tests/test_views.py b/pyramid_openapi3/tests/test_views.py index 882f4b0..94f9b63 100644 --- a/pyramid_openapi3/tests/test_views.py +++ b/pyramid_openapi3/tests/test_views.py @@ -16,6 +16,7 @@ from pyramid.testing import testConfig from pyramid_openapi3.exceptions import RequestValidationError +import json import os import pytest import tempfile @@ -126,6 +127,38 @@ def test_add_spec_view() -> None: assert view(request=None, context=None).body == MINIMAL_DOCUMENT +def test_add_spec_view_json() -> None: + """Test that the openapi document is also served as JSON, alongside YAML.""" + with testConfig() as config: + config.include("pyramid_openapi3") + + with tempfile.NamedTemporaryFile() as document: + document.write(MINIMAL_DOCUMENT) + document.seek(0) + + config.pyramid_openapi3_spec( + document.name, route="/foo.yaml", route_name="foo_api_spec" + ) + + # assert route + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + assert routes[1].name == "foo_api_spec_json" + assert routes[1].path == "/foo.json" + + # assert view + request = config.registry.queryUtility( + IRouteRequest, name="foo_api_spec_json" + ) + view = config.registry.adapters.registered( + (IViewClassifier, request, Interface), IView, name="" + ) + response = view(request=None, context=None) + assert response.content_type == "application/json" + spec = config.registry.settings["pyramid_openapi3"]["spec"] + assert json.loads(response.body) == spec.read_value() + + def test_add_spec_view_already_defined() -> None: """Test that creating a spec more than once raises an Exception.""" with testConfig() as config: