Source code for dmr.openapi.core.merger

import dataclasses
from typing import TYPE_CHECKING, TypeVar

from dmr.internal.types import EMPTY
from dmr.openapi.objects import Components, Info, Paths
from dmr.openapi.openapi import OpenAPI

if TYPE_CHECKING:
    from dmr.openapi.core.context import OpenAPIContext


[docs] @dataclasses.dataclass(frozen=True, slots=True) class ConfigMerger: """ Merges OpenAPI configuration with generated paths and components. This class is responsible for combining the OpenAPI configuration from the context with the generated paths and components to create a complete OpenAPI specification object. """ context: 'OpenAPIContext'
[docs] def __call__(self, paths: Paths, components: Components) -> OpenAPI: """Merge paths and components with configuration.""" config = self.context.config return OpenAPI( openapi=config.openapi_version, self_uri=config.self_uri, json_schema_dialect=config.json_schema_dialect, info=Info( title=config.title, version=config.version, summary=config.summary, description=config.description, terms_of_service=config.terms_of_service, contact=config.contact, license=config.license, ), servers=config.servers, tags=config.tags, external_docs=config.external_docs, security=(None if config.security is EMPTY else config.security), webhooks=config.webhooks, x_extensions=config.x_extensions, paths=paths, components=self._merge_components(config.components, components), )
def _merge_components( self, existing: Components | list[Components] | None, to_merge: Components, ) -> Components: """Merge :class:`dmr.openapi.objects.Components` defs together.""" if not existing: return to_merge if isinstance(existing, list): temporary: Components | None = None for subcomponents in existing: temporary = self._merge_components(temporary, subcomponents) # for mypy: it can't be nothing but `Components` now: assert isinstance(temporary, Components) # noqa: S101 existing = temporary return Components( schemas=_merge_unique(existing.schemas, to_merge.schemas), responses=_merge_unique(existing.responses, to_merge.responses), parameters=_merge_unique(existing.parameters, to_merge.parameters), examples=_merge_unique(existing.examples, to_merge.examples), request_bodies=_merge_unique( existing.request_bodies, to_merge.request_bodies, ), headers=_merge_unique(existing.headers, to_merge.headers), security_schemes=_merge_unique( existing.security_schemes, to_merge.security_schemes, ), links=_merge_unique(existing.links, to_merge.links), callbacks=_merge_unique(existing.callbacks, to_merge.callbacks), path_items=_merge_unique(existing.path_items, to_merge.path_items), media_types=_merge_unique( existing.media_types, to_merge.media_types, ), )
_ThingT = TypeVar('_ThingT') def _merge_unique( existing: dict[str, _ThingT] | None, to_merge: dict[str, _ThingT] | None, ) -> dict[str, _ThingT] | None: if existing is None: return _sorted_or_none(to_merge) if to_merge is None: return _sorted_or_none(existing) shared_keys = existing.keys() & to_merge.keys() if shared_keys: raise ValueError( f'Trying to merge components with shared keys: {shared_keys}', ) return _sorted_or_none({**existing, **to_merge}) def _sorted_or_none( components: dict[str, _ThingT] | None, ) -> dict[str, _ThingT] | None: if not components: return None # Sorted by name, so the schema does not depend on the definition order: return dict(sorted(components.items()))