Skip to content

Commit c22e325

Browse files
committed
feat: Add opt-in route_json param to serve the spec as JSON
The spec dict is already parsed at registration time, so serving it as JSON is just another view on the same data. Off by default. Resolves #10
1 parent 214e9e8 commit c22e325

3 files changed

Lines changed: 77 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ The reason this package exists is to give you peace of mind when providing a RES
6767
config.pyramid_openapi3_add_explorer(route='/api/v1/')
6868
```
6969

70+
Pass `route_json='/api/v1/openapi.json'` to `pyramid_openapi3_spec` to also serve the spec as JSON (off by default).
71+
7072
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:
7173

7274
```python

pyramid_openapi3/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ def add_spec_view(
257257
filepath: str,
258258
route: str = "/openapi.yaml",
259259
route_name: str = "pyramid_openapi3.spec",
260+
route_json: str | None = None,
260261
permission: str = NO_PERMISSION_REQUIRED,
261262
apiname: str = "pyramid_openapi3",
262263
) -> None:
@@ -265,6 +266,10 @@ def add_spec_view(
265266
:param filepath: absolute/relative path to the specification file
266267
:param route: URL path where to serve specification file
267268
:param route_name: Route name under which specification file will be served
269+
:param route_json: URL path where to serve the specification as JSON. If not
270+
provided, the JSON representation is not served. The route is registered
271+
under ``route_name`` suffixed with ``_json``; not available on
272+
`add_spec_view_directory`.
268273
:param permission: Permission for the spec view
269274
"""
270275

@@ -289,6 +294,20 @@ def spec_view(request: Request) -> FileResponse:
289294
config.add_route(route_name, route)
290295
config.add_view(route_name=route_name, permission=permission, view=spec_view)
291296

297+
if route_json is not None:
298+
route_name_json = f"{route_name}_json"
299+
spec_json = json.dumps(spec_dict)
300+
301+
def spec_view_json(request: Request) -> Response:
302+
return Response(
303+
spec_json, content_type="application/json", charset="UTF-8"
304+
)
305+
306+
config.add_route(route_name_json, route_json)
307+
config.add_view(
308+
route_name=route_name_json, permission=permission, view=spec_view_json
309+
)
310+
292311
config.registry.settings[apiname] = _create_api_settings(
293312
config, filepath, route_name, spec
294313
)

pyramid_openapi3/tests/test_views.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pyramid.testing import testConfig
1717
from pyramid_openapi3.exceptions import RequestValidationError
1818

19+
import json
1920
import os
2021
import pytest
2122
import tempfile
@@ -126,6 +127,61 @@ def test_add_spec_view() -> None:
126127
assert view(request=None, context=None).body == MINIMAL_DOCUMENT
127128

128129

130+
def test_add_spec_view_json_not_served_by_default() -> None:
131+
"""Test that the JSON spec route is not registered unless opted in."""
132+
with testConfig() as config:
133+
config.include("pyramid_openapi3")
134+
135+
with tempfile.NamedTemporaryFile() as document:
136+
document.write(MINIMAL_DOCUMENT)
137+
document.seek(0)
138+
139+
config.pyramid_openapi3_spec(
140+
document.name, route="/foo.yaml", route_name="foo_api_spec"
141+
)
142+
143+
# no route registered for the (guessed) json variant
144+
route_request = config.registry.queryUtility(
145+
IRouteRequest, name="foo_api_spec_json"
146+
)
147+
assert route_request is None
148+
149+
150+
def test_add_spec_view_json() -> None:
151+
"""Test registration of a view that serves the openapi document as JSON."""
152+
with testConfig() as config:
153+
config.include("pyramid_openapi3")
154+
155+
with tempfile.NamedTemporaryFile() as document:
156+
document.write(MINIMAL_DOCUMENT)
157+
document.seek(0)
158+
159+
config.pyramid_openapi3_spec(
160+
document.name,
161+
route="/foo.yaml",
162+
route_name="foo_api_spec",
163+
route_json="/foo.json",
164+
)
165+
166+
# assert route
167+
mapper = config.registry.getUtility(IRoutesMapper)
168+
routes = mapper.get_routes()
169+
assert routes[1].name == "foo_api_spec_json"
170+
assert routes[1].path == "/foo.json"
171+
172+
# assert view
173+
request = config.registry.queryUtility(
174+
IRouteRequest, name="foo_api_spec_json"
175+
)
176+
view = config.registry.adapters.registered(
177+
(IViewClassifier, request, Interface), IView, name=""
178+
)
179+
response = view(request=None, context=None)
180+
assert response.content_type == "application/json"
181+
spec = config.registry.settings["pyramid_openapi3"]["spec"]
182+
assert json.loads(response.body) == spec.read_value()
183+
184+
129185
def test_add_spec_view_already_defined() -> None:
130186
"""Test that creating a spec more than once raises an Exception."""
131187
with testConfig() as config:

0 commit comments

Comments
 (0)