Source code for dmr.openapi.generators.operation

import dataclasses
import re
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from dmr.controller import Controller
    from dmr.metadata import EndpointMetadata
    from dmr.openapi.core.context import OpenAPIContext
    from dmr.serializer import BaseSerializer


[docs] @dataclasses.dataclass(frozen=True, slots=True) class OperationIdGenerator: """ Generator for unique OpenAPI operation IDs. The Operation ID builder is responsible for creating unique operation IDs for OpenAPI operations. It uses the explicit ``operation_id`` from endpoint metadata if available, otherwise generates one from the HTTP method and path following ``RFC 3986`` specifications. All generated operation IDs are registered in the registry to ensure uniqueness across the OpenAPI specification. """ _context: 'OpenAPIContext'
[docs] def __call__( self, path: str, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> str: """ Generate a unique operation ID for an OpenAPI operation. Uses the explicit ``operation_id`` from endpoint metadata if available, otherwise generates one from the HTTP method and path. The operation ID is registered in the registry to ensure uniqueness. .. versionchanged:: 0.16.0 Now accepts *controller_cls* parameter instead of *suffix* and *serializer*. """ operation_id = metadata.operation_id if operation_id is not None: self._context.registries.operation_id.register(operation_id) return operation_id # Generate operation_id from path and method suffix = self.generate_suffix(path, metadata, controller_cls) operation_id = metadata.method.lower() + ''.join( self._tokenize_path(suffix + path), ) self._context.registries.operation_id.register(operation_id) return operation_id
[docs] def generate_suffix( self, path: str, metadata: 'EndpointMetadata', controller_cls: type['Controller[BaseSerializer]'], ) -> str: """ Generate suffix for operation ID. .. versionadded:: 0.16.0 """ return controller_cls.__qualname__
def _tokenize_path(self, path: str) -> list[str]: # noqa: WPS210 """ Tokenize path into meaningful parts for operation ID generation. According to RFC 3986: - Removes path variables (e.g., {id}, {user_id}) - Splits by '/' (gen-delim, path segment separator) - Normalizes unreserved characters: '-', '_', '.', '~' are treated as word separators for camelCase conversion - Removes reserved characters that shouldn't appear in operation IDs """ # Remove `{}` from path variables (e.g., {id}, {user_id}) path = path.replace('{', '').replace('}', '') tokenized_path = path.strip('/').split('/') normalized_tokens: list[str] = [] for token in tokenized_path: if not token: continue # Remove reserved characters (gen-delims and sub-delims) # Keep only unreserved: ALPHA, DIGIT, '-', '.', '_', '~' # (RFC 3986, section 2.3) cleaned_token = re.sub(pattern=r'[^\w\-._~]', repl='', string=token) if not cleaned_token: continue # Split to preserve word boundaries parts = re.split(r'[-_.~]+', cleaned_token) normalized_parts = [ part[0].upper() + part[1:] for part in parts if part ] if normalized_parts: normalized_tokens.append(''.join(normalized_parts)) return normalized_tokens