1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
"""This submodule contains a JSON reference translator."""
__author__ = "Štěpán Tomsa"
__copyright__ = "Copyright © 2021 Štěpán Tomsa"
__license__ = "MIT"
__all__ = ()
import prance.util.url as _url
def _reference_key(ref_url, item_path):
"""
Return a portion of the dereferenced URL.
format - ref-url_obj-path
"""
return ref_url.path.split("/")[-1] + "_" + "_".join(item_path[1:])
def _local_ref(path):
url = "#/" + "/".join(path)
return {"$ref": url}
# Underscored to allow some time for the public API to be stabilized.
class _RefTranslator:
"""
Resolve JSON pointers/references in a spec by translation.
References to objects in other files are copied to the /components/schemas
object of the root document, while being translated to point to the the new
object locations.
"""
def __init__(self, specs, url):
"""
Construct a JSON reference translator.
The translated specs are in the `specs` member after a call to
`translate_references` has been made.
If a URL is given, it is used as a base for calculating the absolute
URL of relative file references.
:param dict specs: The parsed specs in which to translate any references.
:param str url: [optional] The URL to base relative references on.
"""
import copy
self.specs = copy.deepcopy(specs)
self.__strict = True
self.__reference_cache = {}
self.__collected_references = {}
if url:
self.url = _url.absurl(url)
url_key = (_url.urlresource(self.url), self.__strict)
# If we have a url, we want to add ourselves to the reference cache
# - that creates a reference loop, but prevents child resolvers from
# creating a new resolver for this url.
self.__reference_cache[url_key] = self.specs
else:
self.url = None
def translate_references(self):
"""
Iterate over the specification document, performing the translation.
Traverses over the whole document, adding the referenced object from
external files to the /components/schemas object in the root document
and translating the references to the new location.
"""
self.specs = self._translate_partial(self.url, self.specs)
# Add collected references to the root document.
if self.__collected_references:
if "components" not in self.specs:
self.specs["components"] = {}
if "schemas" not in self.specs["components"]:
self.specs["components"].update({"schemas": {}})
self.specs["components"]["schemas"].update(self.__collected_references)
def _dereference(self, ref_url, obj_path):
"""
Dereference the URL and object path.
Returns the dereferenced object.
:param mixed ref_url: The URL at which the reference is located.
:param list obj_path: The object path within the URL resource.
:param tuple recursions: A recursion stack for resolving references.
:return: A copy of the dereferenced value, with all internal references
resolved.
"""
# In order to start dereferencing anything in the referenced URL, we have
# to read and parse it, of course.
contents = _url.fetch_url(ref_url, self.__reference_cache, strict=self.__strict)
# In this inner parser's specification, we can now look for the referenced
# object.
value = contents
if len(obj_path) != 0:
from prance.util.path import path_get
try:
value = path_get(value, obj_path)
except (KeyError, IndexError, TypeError) as ex:
raise _url.ResolutionError(
f'Cannot resolve reference "{ref_url.geturl()}": {str(ex)}'
)
# Deep copy value; we don't want to create recursive structures
import copy
value = copy.deepcopy(value)
# Now resolve partial specs
value = self._translate_partial(ref_url, value)
# That's it!
return value
def _translate_partial(self, base_url, partial):
changes = dict(tuple(self._translating_iterator(base_url, partial, ())))
paths = sorted(changes.keys(), key=len)
from prance.util.path import path_set
for path in paths:
value = changes[path]
if len(path) == 0:
partial = value
else:
path_set(partial, list(path), value, create=True)
return partial
def _translating_iterator(self, base_url, partial, path):
from prance.util.iterators import reference_iterator
for _, ref_string, item_path in reference_iterator(partial):
ref_url, obj_path = _url.split_url_reference(base_url, ref_string)
full_path = path + item_path
if ref_url.path == self.url.path:
# Reference to the root document.
ref_path = obj_path
else:
# Reference to a non-root document.
ref_key = _reference_key(ref_url, obj_path)
if ref_key not in self.__collected_references:
self.__collected_references[ref_key] = None
ref_value = self._dereference(ref_url, obj_path)
self.__collected_references[ref_key] = ref_value
ref_path = ["components", "schemas", ref_key]
ref_obj = _local_ref(ref_path)
yield full_path, ref_obj
|