Source code for action0.openapi.resolve

"""
Resolution of local ``$ref`` pointers inside an OpenAPI document.

OpenAPI schemas reference shared definitions as JSON pointers like
``#/components/schemas/Pet``. :py:class:`RefResolver` looks such
pointers up in the loaded document and follows chains of them, so the
translation stage can work with plain schema objects. Only *local*
references (into the same document) are supported — remote and file
references raise :py:class:`~action0.openapi.errors.SchemaError`.
"""

from collections.abc import Mapping
from collections.abc import Sequence
from typing import Any

from .errors import SchemaError


[docs] class RefResolver: """ Looks up local ``$ref`` JSON pointers in one OpenAPI document. :param document: the loaded schema document """ def __init__(self, document: Mapping[str, Any]) -> None: self._document = document
[docs] def lookup(self, ref: str) -> Any: """ Return the node a local JSON pointer refers to. >>> resolver = RefResolver({"components": {"schemas": {"Pet": {"type": "object"}}}}) >>> resolver.lookup("#/components/schemas/Pet") {'type': 'object'} :param ref: the pointer, e.g. ``#/components/schemas/Pet`` :return: the referenced node :raises SchemaError: if the pointer is not local or does not resolve """ if not ref.startswith("#/"): raise SchemaError( f"unsupported reference {ref!r} — only local references" ' ("#/...") into the same document are supported' ) node: Any = self._document for token in ref[2:].split("/"): token = _unescape(token) if isinstance(node, Mapping): if token not in node: raise SchemaError(f"broken reference {ref!r}: {token!r} does not exist") node = node[token] elif isinstance(node, Sequence) and not isinstance(node, str): try: node = node[int(token)] except (ValueError, IndexError): raise SchemaError( f"broken reference {ref!r}: {token!r} is not a valid list index" ) from None else: raise SchemaError(f"broken reference {ref!r}: {token!r} cannot be looked up") return node
[docs] def deref(self, node: Mapping[str, Any]) -> Mapping[str, Any]: """ Follow a (chain of) ``$ref`` to the actual schema object. A node without ``$ref`` is returned as-is, so this is safe to call on every schema-shaped node. >>> resolver = RefResolver({"components": {"schemas": {"Pet": {"type": "object"}}}}) >>> resolver.deref({"$ref": "#/components/schemas/Pet"}) {'type': 'object'} >>> resolver.deref({"type": "string"}) {'type': 'string'} :param node: a schema node that may be a reference :return: the referenced (or given) schema object :raises SchemaError: on non-local, broken or circular references, or if the target is not an object """ seen: list[str] = [] while "$ref" in node: ref = node["$ref"] if not isinstance(ref, str): raise SchemaError(f"invalid $ref value {ref!r} — must be a string") if ref in seen: raise SchemaError(f"circular reference chain: {' -> '.join([*seen, ref])}") seen.append(ref) target = self.lookup(ref) if not isinstance(target, Mapping): raise SchemaError(f"reference {ref!r} does not point at an object") node = target return node
[docs] @staticmethod def ref_name(ref: str) -> str: """ Return the last pointer segment — the component's name. >>> RefResolver.ref_name("#/components/schemas/Pet") 'Pet' :param ref: the pointer :return: the unescaped final segment """ return _unescape(ref.rsplit("/", 1)[-1])
def _unescape(token: str) -> str: """ Undo RFC 6901 escaping in one pointer segment. :param token: the raw segment :return: the segment with ``~1`` as ``/`` and ``~0`` as ``~`` """ return token.replace("~1", "/").replace("~0", "~")