diff --git a/prance/__init__.py b/prance/__init__.py index 505cb34..6a1caf9 100644 --- a/prance/__init__.py +++ b/prance/__init__.py @@ -282,7 +282,7 @@ def _validate(self): # We therefore use our own resolver first, and validate later. from .util.resolver import RefResolver forward_arg_names = ('encoding', 'recursion_limit', - 'recursion_limit_handler', 'resolve_types') + 'recursion_limit_handler', 'resolve_types', 'resolve_method') forward_args = { k: v for (k, v) in self.options.items() if k in forward_arg_names } diff --git a/prance/util/resolver.py b/prance/util/resolver.py index 3b659c1..0dcc440 100644 --- a/prance/util/resolver.py +++ b/prance/util/resolver.py @@ -15,6 +15,11 @@ #: Resolve references to local files. RESOLVE_FILES = 2 ** 3 +#: Copy the schema changing the reference. +TRANSLATE_EXTERNAL = 0 +#: Replace the reference with inlined schema. +TRANSLATE_DEFAULT = 1 + #: Default, resole all references. RESOLVE_ALL = RESOLVE_INTERNAL | RESOLVE_HTTP | RESOLVE_FILES @@ -66,6 +71,9 @@ def __init__(self, specs, url = None, **options): detect_encoding is used to determine the encoding. :param int resolve_types: [optional] Specify which types of references to resolve. Defaults to RESOLVE_ALL. + :param int resolve_method: [optional] Specify whether to translate external + references in components/schemas or dereference in place. Defaults + to TRANSLATE_DEFAULT. """ import copy self.specs = copy.deepcopy(specs) @@ -89,11 +97,20 @@ def __init__(self, specs, url = None, **options): self.parsed_url = self._url_key = None self.__resolve_types = options.get('resolve_types', RESOLVE_ALL) + self.__resolve_method = options.get('resolve_method', TRANSLATE_DEFAULT) self.__encoding = options.get('encoding', None) + self.__soft_dereference_objs = {} def resolve_references(self): """Resolve JSON pointers/references in the spec.""" self.specs = self._resolve_partial(self.parsed_url, self.specs, ()) + + # If there are any objects collected when using TRANSLATE_EXTERNAL, add them to components/schemas + if self.__soft_dereference_objs: + if "components" not in self.specs: self.specs["components"] = dict() + if "schemas" not in self.specs["components"]: self.specs["components"].update({"schemas":{}}) + + self.specs["components"]["schemas"].update(self.__soft_dereference_objs) def _dereferencing_iterator(self, base_url, partial, path, recursions): """ @@ -111,6 +128,8 @@ def _dereferencing_iterator(self, base_url, partial, path, recursions): # Split the reference string into parsed URL and object path ref_url, obj_path = _url.split_url_reference(base_url, refstring) + translate = self.__resolve_method == TRANSLATE_EXTERNAL and self.parsed_url.path != ref_url.path + if self._skip_reference(base_url, ref_url): continue @@ -136,7 +155,20 @@ def _dereferencing_iterator(self, base_url, partial, path, recursions): full_path = path + item_path # First yield parent - yield full_path, ref_value + if translate: + url = self._collect_soft_refs(ref_url, obj_path, ref_value) + yield full_path, {"$ref": "#/components/schemas/"+url} + else: + yield full_path, ref_value + + def _collect_soft_refs(self, ref_url, item_path, value): + """ + Returns a portion of the dereferenced url for TRANSLATE_EXTERNAL mode. + format - ref-url_obj-path + """ + dref_url = ref_url.path.split("/")[-1]+"_"+"_".join(item_path[1:]) + self.__soft_dereference_objs[dref_url] = value + return dref_url def _skip_reference(self, base_url, ref_url): """Return whether the URL should not be dereferenced.""" diff --git a/tests/test_util_resolver.py b/tests/test_util_resolver.py index 35d7adc..d2b4c37 100644 --- a/tests/test_util_resolver.py +++ b/tests/test_util_resolver.py @@ -494,3 +494,94 @@ def test_issue_78_resolve_internal_bug(): assert 'application/json' in val # Internal reference within file is NOT resolved assert '$ref' in val['application/json'] + + + +@pytest.mark.skipif(none_of('openapi-spec-validator'), reason='Missing backends') +def test_issue_77_translate_external(): + specs = '' + with open('tests/specs/issue_78/openapi.json', 'r') as fh: + specs = fh.read() + + from prance.util import formats + specs = formats.parse_spec(specs, 'openapi.json') + + res = resolver.RefResolver(specs, + fs.abspath('openapi.json'), + resolve_types = resolver.RESOLVE_FILES, + resolve_method= resolver.TRANSLATE_EXTERNAL + ) + res.resolve_references() + + from prance.util.path import path_get + val = path_get(res.specs, ('components', 'schemas', '_schemas.json_Body')) + + # Reference to file is translated in components/schemas + assert 'content' in val + assert 'application/json' in val['content'] + + # Reference url is updated + val = path_get(res.specs, ('paths', '/endpoint', 'post', 'requestBody', '$ref')) + assert val == '#/components/schemas/_schemas.json_Body' + + + +@pytest.mark.skipif(none_of('openapi-spec-validator'), reason='Missing backends') +def test_issue_77_translate_external_refs_internal(): + specs = '' + with open('tests/specs/issue_78/openapi.json', 'r') as fh: + specs = fh.read() + + from prance.util import formats + specs = formats.parse_spec(specs, 'openapi.json') + + res = resolver.RefResolver(specs, + fs.abspath('openapi.json'), + resolve_types = resolver.RESOLVE_FILES | resolver.RESOLVE_INTERNAL, + resolve_method= resolver.TRANSLATE_EXTERNAL + ) + res.resolve_references() + + from prance.util.path import path_get + val = path_get(res.specs, ('components', 'schemas', '_schemas.json_Body')) + + # Reference to file is translated in components/schemas + assert 'content' in val + assert 'application/json' in val['content'] + + # Internal Reference links updated + assert '#/components/schemas/_schemas.json_Something' == val['content']['application/json']['$ref'] + + # Internal references is copied to componnents/schemas seperately + val = path_get(res.specs, ('components', 'schemas', '_schemas.json_Something')) + assert 'type' in val + + # File reference url is updated as well + val = path_get(res.specs, ('paths', '/endpoint', 'post', 'requestBody', '$ref')) + assert val == '#/components/schemas/_schemas.json_Body' + + +@pytest.mark.skipif(none_of('openapi-spec-validator'), reason='Missing backends') +def test_issue_77_internal_refs_unresolved(): + specs = '' + with open('tests/specs/issue_78/openapi.json', 'r') as fh: + specs = fh.read() + + from prance.util import formats + specs = formats.parse_spec(specs, 'openapi.json') + + res = resolver.RefResolver(specs, + fs.abspath('openapi.json'), + resolve_types = resolver.RESOLVE_FILES, + resolve_method= resolver.TRANSLATE_EXTERNAL + ) + res.resolve_references() + + from prance.util.path import path_get + val = path_get(res.specs, ('components', 'schemas')) + + # File reference resolved + assert '_schemas.json_Body' in val + + # Internal file reference not resolved + assert '_schemas.json_Something' not in val