Skip to content

Commit

Permalink
Merge pull request #82 from tahmidefaz/issue-77
Browse files Browse the repository at this point in the history
Issue 77 - Add Translation Feature
  • Loading branch information
jfinkhaeuser authored Dec 7, 2020
2 parents 0bf40f7 + 0558322 commit 5b08370
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 2 deletions.
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

#: 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

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

0 comments on commit 5b08370

Please sign in to comment.