Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 77 - Add Translation Feature #82

Merged
2 changes: 1 addition & 1 deletion prance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
34 changes: 33 additions & 1 deletion prance/util/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +19 to +21
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names are strange, but I know, naming is hard. This switches the handling of external references. One value is to TRANSLATE and the other is to INLINE.


#: Default, resole all references.
RESOLVE_ALL = RESOLVE_INTERNAL | RESOLVE_HTTP | RESOLVE_FILES

Expand Down Expand Up @@ -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)
Expand All @@ -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):
"""
Expand All @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translate variable is initialized here, but used only down there in the yield condition. I know this is a remnant from the original approach, but now it is only confusing.


if self._skip_reference(base_url, ref_url):
continue

Expand All @@ -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."""
Expand Down
91 changes: 91 additions & 0 deletions tests/test_util_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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