File: translator.py

package info (click to toggle)
python-prance 25.4.8.0%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,140 kB
  • sloc: python: 3,381; makefile: 205
file content (162 lines) | stat: -rw-r--r-- 5,670 bytes parent folder | download
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