diff --git a/sanic_ext/extensions/openapi/blueprint.py b/sanic_ext/extensions/openapi/blueprint.py index 7bd164b..b5b8691 100644 --- a/sanic_ext/extensions/openapi/blueprint.py +++ b/sanic_ext/extensions/openapi/blueprint.py @@ -1,4 +1,5 @@ import inspect +from copy import deepcopy from functools import lru_cache, partial from os.path import abspath, dirname, realpath @@ -192,6 +193,10 @@ def build_spec(app): ): operation.autodoc(docstring) + # Create an operation per method so defaults (like operationId) + # do not overwrite each other for multi-method routes. + operation = deepcopy(operation) + operation._default["operationId"] = ( f"{method.lower()}~{route_name}" ) diff --git a/sanic_ext/utils/route.py b/sanic_ext/utils/route.py index d5e6e46..0a643da 100644 --- a/sanic_ext/utils/route.py +++ b/sanic_ext/utils/route.py @@ -111,7 +111,8 @@ def get_all_routes(app, skip_prefix): continue method_handlers = [ - (method, route.handler) for method in route.methods + (method, route.handler) + for method in sorted(route.methods) ] _, name = route.name.split(".", 1) diff --git a/tests/extensions/openapi/test_operation_id.py b/tests/extensions/openapi/test_operation_id.py new file mode 100644 index 0000000..0e3e862 --- /dev/null +++ b/tests/extensions/openapi/test_operation_id.py @@ -0,0 +1,15 @@ +from sanic import Sanic + +from .utils import get_spec + + +def test_operation_id_is_unique_per_method_and_deterministic(app: Sanic): + @app.route("/ping", methods=["GET", "POST"]) + async def ping(_): + return {"ping": "pong"} + + spec = get_spec(app) + path = spec["paths"]["/ping"] + + assert path["get"]["operationId"] == "get~ping" + assert path["post"]["operationId"] == "post~ping"