From ec6ff855fb17e457a6a0aa28010fab3ea9a3fb61 Mon Sep 17 00:00:00 2001 From: Vitaliy Kucheriavyi Date: Sun, 20 Dec 2020 12:37:27 +0200 Subject: [PATCH] OperationID support (#54) --- docs/docs/tutorial/operation_params.md | 36 +++++++++++++++++++++++++- ninja/main.py | 18 +++++++++++++ ninja/openapi/schema.py | 2 ++ ninja/operation.py | 5 ++++ ninja/router.py | 14 ++++++++++ tests/test_openapi_params.py | 7 ++++- 6 files changed, 80 insertions(+), 2 deletions(-) diff --git a/docs/docs/tutorial/operation_params.md b/docs/docs/tutorial/operation_params.md index 2c57d3a4f..560ed4ed1 100644 --- a/docs/docs/tutorial/operation_params.md +++ b/docs/docs/tutorial/operation_params.md @@ -75,11 +75,45 @@ def create_order(request, order: Order): ![Summary`](../img/operation_description_docstring.png) +## OpenAPI operationId + +OpenAPI `operationId` is an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API. + +By default Django Ninja sets it to `module name` + `function name` + +if you want ot set it individually for each operation use `operation_id` argument: + +```Python hl_lines="2" +... +@api.post("/tasks", operation_id="create_task") +def new_task(request): + ... +``` + +And if you want ot override global behavior - you can inherit NinjaAPI instance and override `get_openapi_operation_id` method + +it will be called to each operation, that you defined, so you can set your custom naming logic + +```Python hl_lines="5 6 7 9" +from ninja import NinjaAPI + +class MySuperApi(NinjaAPI): + + def get_openapi_operation_id(self, operation): + # here you can access operation ( .path , .view_func, etc) + return ... + +api = MySuperApi() + +@api.get(...) +... +``` + ## Deprecating Operation -If you need to mark an operation as deprecated without removing it, use the argumetn `deprecated`: +If you need to mark an operation as deprecated without removing it, use the argument `deprecated`: ```Python hl_lines="1" @api.post("/make-order/", deprecated=True) diff --git a/ninja/main.py b/ninja/main.py index 47a67bd0d..fa96f3e69 100644 --- a/ninja/main.py +++ b/ninja/main.py @@ -1,4 +1,5 @@ import os +from ninja import openapi from ninja.openapi import get_schema from typing import List, Optional, Tuple, Sequence, Union, Callable from django.urls import reverse @@ -45,6 +46,7 @@ def get( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -54,6 +56,7 @@ def get( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -66,6 +69,7 @@ def post( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -75,6 +79,7 @@ def post( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -87,6 +92,7 @@ def delete( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -96,6 +102,7 @@ def delete( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -108,6 +115,7 @@ def patch( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -117,6 +125,7 @@ def patch( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -129,6 +138,7 @@ def put( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -138,6 +148,7 @@ def put( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -151,6 +162,7 @@ def api_operation( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -161,6 +173,7 @@ def api_operation( path, auth=auth is NOT_SET and self.auth or auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -200,6 +213,11 @@ def get_openapi_schema(self, path_prefix=None): path_prefix = self.root_path return get_schema(api=self, path_prefix=path_prefix) + def get_openapi_operation_id(self, operation: "Operation"): + name = operation.view_func.__name__ + module = operation.view_func.__module__ + return (module + "_" + name).replace(".", "_") + def _validate(self): from ninja.security import APIKeyCookie diff --git a/ninja/openapi/schema.py b/ninja/openapi/schema.py index 5ec4a4693..ae16d209e 100644 --- a/ninja/openapi/schema.py +++ b/ninja/openapi/schema.py @@ -56,7 +56,9 @@ def methods(self, operations: list): return result def operation_details(self, operation: Operation): + op_id = operation.operation_id or self.api.get_openapi_operation_id(operation) result = { + "operationId": op_id, "summary": operation.summary, "parameters": self.operation_parameters(operation), "responses": self.responses(operation), diff --git a/ninja/operation.py b/ninja/operation.py index 936276641..9d8beecba 100644 --- a/ninja/operation.py +++ b/ninja/operation.py @@ -19,6 +19,7 @@ def __init__( *, auth: Optional[Union[Sequence[Callable], Callable, object]] = NOT_SET, response: Any = None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -42,6 +43,7 @@ def __init__( else: self.response_model = self._create_response_model(response) + self.operation_id = operation_id self.summary = summary or self.view_func.__name__.title().replace("_", " ") self.description = description or self.signature.docstring self.tags = tags @@ -171,6 +173,7 @@ def add( *, auth: Optional[Union[Sequence[Callable], Callable, object]] = NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -184,6 +187,7 @@ def add( view_func, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -196,6 +200,7 @@ def add( view_func, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, diff --git a/ninja/router.py b/ninja/router.py index 61b43f7e2..976a58f75 100644 --- a/ninja/router.py +++ b/ninja/router.py @@ -20,6 +20,7 @@ def get( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -30,6 +31,7 @@ def get( path, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -42,6 +44,7 @@ def post( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -52,6 +55,7 @@ def post( path, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -64,6 +68,7 @@ def delete( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -74,6 +79,7 @@ def delete( path, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -86,6 +92,7 @@ def patch( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -96,6 +103,7 @@ def patch( path, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -108,6 +116,7 @@ def put( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -118,6 +127,7 @@ def put( path, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -131,6 +141,7 @@ def api_operation( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -143,6 +154,7 @@ def decorator(view_func): view_func, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, @@ -160,6 +172,7 @@ def add_api_operation( *, auth=NOT_SET, response=None, + operation_id: Optional[str] = None, summary: Optional[str] = None, description: Optional[str] = None, tags: Optional[List[str]] = None, @@ -176,6 +189,7 @@ def add_api_operation( view_func=view_func, auth=auth, response=response, + operation_id=operation_id, summary=summary, description=description, tags=tags, diff --git a/tests/test_openapi_params.py b/tests/test_openapi_params.py index 1bfb30a2c..06ab14e88 100644 --- a/tests/test_openapi_params.py +++ b/tests/test_openapi_params.py @@ -4,7 +4,7 @@ api = NinjaAPI() -@api.get("/operation1") +@api.get("/operation1", operation_id="my_id") def operation_1(request): """ This will be in description @@ -39,6 +39,11 @@ def test_schema(): op4 = schema["paths"]["/api/operation4"]["get"] pprint(op1) + assert op1["operationId"] == "my_id" + assert op2["operationId"] == "test_openapi_params_operation2" + assert op3["operationId"] == "test_openapi_params_operation3" + assert op4["operationId"] == "test_openapi_params_operation4" + assert op1["summary"] == "Operation 1" assert op2["summary"] == "Operation2" assert op3["summary"] == "Summary from argument"