From ea87ac82b3d2a46910420f3c2f66866f0bdad8d0 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 2 Dec 2014 20:09:28 +0800 Subject: [PATCH 01/21] add path2url --- pyswagger/tests/test_utils.py | 5 +++++ pyswagger/utils.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 92d3a07..5e1bacc 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -95,3 +95,8 @@ def test_import_string(self): """ test import_string """ self.assertEqual(utils.import_string('qoo_%^&%&'), None) self.assertNotEqual(utils.import_string('pyswagger'), None) + + def test_path2url(self): + """ test path2url """ + self.assertEqual(utils.path2url('/opt/local/a.json'), 'file:///opt/local/a.json') + diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 1989403..c1eaa2b 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -243,3 +243,20 @@ def nv_tuple_list_replace(l, v): if not _found: l.append(v) +def path2url(p): + """ Return file:// URL from a filename. + """ + return six.moves.urllib.parse.urljoin( + 'file:', six.moves.urllib.request.pathname2url(p) + ) + +def get_swagger_version(obj): + """ get swagger version from loaded json """ + + # TODO: test case + if isinstance(obj, dict): + return obj['swaggerVersion'] if 'swaggerVersion' in obj else obj['swagger'] + else: + # should be an instance of BaseObj + return obj.swaggerVersion if hasattr(obj, 'swaggerVersion') else obj.swagger + From 11657a229b85221103662d8023967a3448837ff0 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 2 Dec 2014 23:11:06 +0800 Subject: [PATCH 02/21] prepare for resolve external $ref --- pyswagger/core.py | 197 +++++++++++++++++++++++++++----------------- pyswagger/getter.py | 9 +- 2 files changed, 127 insertions(+), 79 deletions(-) diff --git a/pyswagger/core.py b/pyswagger/core.py index 358b197..3c38c38 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -1,19 +1,12 @@ from __future__ import absolute_import -from .getter import HttpGetter, FileGetter +from .getter import UrlGetter, LocalGetter from .spec.v1_2.parser import ResourceListContext from .spec.v2_0.parser import SwaggerContext from .scan import Scanner from .scanner import TypeReduce from .scanner.v1_2 import Upgrade from .scanner.v2_0 import AssignParent, Resolve, PatchObject -from .utils import ( - ScopeDict, - import_string, - jp_compose, - jp_split, - get_dict_as_tuple, - nv_tuple_list_replace - ) +from pyswagger import utils import inspect import base64 import six @@ -36,11 +29,14 @@ class SwaggerApp(object): def __init__(self): """ constructor """ + self.__root_url = None self.__root = None self.__raw = None + self.__version = '' + self.__op = None self.__m = None - self.__version = '' + self.__url2obj = {} self.__schemes = [] # TODO: allow init App-wised SCOPE_SEPARATOR @@ -73,16 +69,16 @@ def raw(self): @property def op(self): - """ list of Operations, organized by ScopeDict + """ list of Operations, organized by utilsScopeDict - :type: ScopeDict of Operations + :type: utils.ScopeDict of Operations """ return self.__op @property def m(self): """ backward compatible, convert - SwaggerApp.d to ScopeDict + SwaggerApp.d to utils.ScopeDict """ return self.__m @@ -98,71 +94,70 @@ def schemes(self): """ return self.__schemes - @classmethod - def load(kls, url, getter=None): - """ load json as a raw SwaggerApp - - :param str url: url of path of Swagger API definition - :param getter: customized Getter - :type getter: sub class/instance of Getter - :return: the created SwaggerApp object - :rtype: SwaggerApp - :raises ValueError: if url is wrong - :raises NotImplementedError: the swagger version is not supported. + @staticmethod + def _prepare_url(url): """ + """ + getter = UrlGetter - app = kls() - - local_getter = getter or HttpGetter p = six.moves.urllib.parse.urlparse(url) if p.scheme == "": if p.netloc == "" and p.path != "": # it should be a file path - local_getter = FileGetter(p.path) + getter = LocalGetter(p.path) + url = utils.path2url(url) else: raise ValueError('url should be a http-url or file path -- ' + url) - else: - app.schemes.append(p.scheme) - if inspect.isclass(local_getter): + if inspect.isclass(getter): # default initialization is passing the url # you can override this behavior by passing an # initialized getter object. - local_getter = local_getter(url) + getter = getter(url) - tmp = {'_tmp_': {}} + return getter, url, p.scheme + + def __load_json(self, url, getter=None, parser=None): + """ + """ + if not getter: + getter, url, _ = self._prepare_url(url) + + if url in self.__url2obj: + # look into cache first + return self.__url2obj[url] # get root document to check its swagger version. - obj, _ = six.advance_iterator(local_getter) - if 'swaggerVersion' in obj and obj['swaggerVersion'] == '1.2': - # swagger 1.2 - with ResourceListContext(tmp, '_tmp_') as ctx: - ctx.parse(local_getter, obj) - - setattr(app, '_' + kls.__name__ + '__version', '1.2') - elif 'swagger' in obj: - if obj['swagger'] == '2.0': + obj, _ = six.advance_iterator(getter) + tmp = {'_tmp_': {}} + if not parser: + if utils.get_swagger_version(obj) == '1.2': + # swagger 1.2 + with ResourceListContext(tmp, '_tmp_') as ctx: + ctx.parse(getter, obj) + else: # swagger 2.0 with SwaggerContext(tmp, '_tmp_') as ctx: ctx.parse(obj) - - setattr(app, '_' + kls.__name__ + '__version', '2.0') - else: - raise NotImplementedError('Unsupported Version: {0}'.format(obj['swagger'])) else: - raise LookupError('Unable to find swagger version') + with parser(tmp, '_tmp_') as ctx: + ctx.parse(obj) - setattr(app, '_' + kls.__name__ + '__raw', tmp['_tmp_']) - return app + # update map of url to obj + self.__url2obj.update({ + url: tmp['_tmp_'] + }) - def validate(self, strict=True): + return tmp['_tmp_'] + + def __validate(self, url=None): """ check if this Swagger API valid or not. :param bool strict: when in strict mode, exception would be raised if not valid. :return: validation errors :rtype: list of tuple(where, type, msg). """ - v_mod = import_string('.'.join([ + v_mod = utils.import_string('.'.join([ 'pyswagger', 'scanner', 'v' + self.version.replace('.', '_'), @@ -172,50 +167,104 @@ def validate(self, strict=True): if not v_mod: # there is no validation module # for this version of spec - return + return [] s = Scanner(self) v = v_mod.Validate() - s.scan(route=[v], root=self.__raw) - - if strict and len(v.errs) > 0: - raise ValueError('this Swagger App contains error: {0}.'.format(len(v.errs))) + if url: + s.scan(route=[v], root=self.__url2obj[url]) + else: + s.scan(route=[v], root=self.__raw) return v.errs - def prepare(self): - """ preparation for loaded json + def __prepare_obj(self, url): + """ """ - s = Scanner(self) - self.validate() + self.__validate(url=url) + obj = self.__url2obj[url] if self.version == '1.2': converter = Upgrade() - s.scan(root=self.__raw, route=[converter]) - self.__root = converter.swagger + s.scan(root=obj, route=[converter]) + obj = converter.swagger # We only have to run this scanner when upgrading from 1.2. # Mainly because we initial BaseObj via NullContext - s.scan(root=self.__root, route=[AssignParent()]) + s.scan(root=obj, route=[AssignParent()]) elif self.version == '2.0': - self.__root = self.__raw + pass else: raise NotImplementedError('Unsupported Version: {0}'.format(self.__version)) - - # update schemes if any - if self.__root.schemes and len(self.__root.schemes) > 0: - self.__schemes = self.__root.schemes + + # back to cache + self.__url2obj[url] = obj + + if url == self.__root_url: + # TODO: ugly... + self.__root = obj + # update schemes if any + if self.__root.schemes and len(self.__root.schemes) > 0: + self.__schemes = self.__root.schemes + + s.scan(root=obj, route=[Resolve(), PatchObject()]) + + @classmethod + def load(kls, url, getter=None): + """ load json as a raw SwaggerApp + + :param str url: url of path of Swagger API definition + :param getter: customized Getter + :type getter: sub class/instance of Getter + :return: the created SwaggerApp object + :rtype: SwaggerApp + :raises ValueError: if url is wrong + :raises NotImplementedError: the swagger version is not supported. + """ + local_getter, url, scheme = kls._prepare_url(url) + getter = getter or local_getter + app = kls() + obj = app.__load_json(url, getter) + + setattr(app, '_' + kls.__name__ + '__root_url', url) + setattr(app, '_' + kls.__name__ + '__version', utils.get_swagger_version(obj)) + setattr(app, '_' + kls.__name__ + '__raw', obj) + app.schemes.append(scheme) + + return app + + def validate(self, strict=True): + """ check if this Swagger API valid or not. + + :param bool strict: when in strict mode, exception would be raised if not valid. + :return: validation errors + :rtype: list of tuple(where, type, msg). + """ + errs = self.__validate() + if strict and len(errs): + raise ValueError('this Swagger App contains error: {0}.'.format(len(errs))) + + return errs + + def prepare(self): + """ preparation for loaded json + """ + + self.__prepare_obj(url=self.__root_url) + self.__root = self.__url2obj[self.__root_url] + + s = Scanner(self) # reducer for Operation tr = TypeReduce() - s.scan(root=self.__root, route=[tr, Resolve(), PatchObject()]) + s.scan(root=self.__root, route=[tr]) # 'op' -- shortcut for Operation with tag and operaionId - self.__op = ScopeDict(tr.op) + self.__op = utils.ScopeDict(tr.op) # 'm' -- shortcut for model in Swagger 1.2 - self.__m = ScopeDict(self.__root.definitions) + self.__m = utils.ScopeDict(self.__root.definitions) @classmethod def create(kls, url, getter=None): @@ -257,7 +306,7 @@ def resolve(self, path): if path.endswith('/'): path = path[:-1] - obj = self.root.resolve(jp_split(path)[1:]) # heading element is #, mapping to self.root + obj = self.root.resolve(utils.jp_split(path)[1:]) # heading element is #, mapping to self.root if obj == None: raise ValueError('Unable to resolve path, [{0}]'.format(path)) @@ -269,7 +318,7 @@ def resolve(self, path): def s(self, p, b=_shortcut_[sc_path]): """ shortcut to access Objects """ - return self.resolve(jp_compose(p, base=b)) + return self.resolve(utils.jp_compose(p, base=b)) class SwaggerSecurity(object): @@ -332,7 +381,7 @@ def __call__(self, req): if header: req._p['header'].update(cred) else: - nv_tuple_list_replace(req._p['query'], get_dict_as_tuple(cred)) + utils.nv_tuple_list_replace(req._p['query'], utils.get_dict_as_tuple(cred)) return req diff --git a/pyswagger/getter.py b/pyswagger/getter.py index 34710d6..ca0d989 100644 --- a/pyswagger/getter.py +++ b/pyswagger/getter.py @@ -34,7 +34,6 @@ def __next__(self): # find urls to retrieve from resource listing file if name == '': urls = self.__find_urls(obj) - # TODO: not worked in DictGetter self.urls.extend(zip( map(lambda u: self.base_path + u, urls), map(lambda u: u[1:], urls) @@ -68,11 +67,11 @@ def __find_urls(self, obj): return urls -class FileGetter(Getter): +class LocalGetter(Getter): """ default getter implmenetation for local resource file """ def __init__(self, path): - super(FileGetter, self).__init__(path) + super(LocalGetter, self).__init__(path) for n in const.SWAGGER_FILE_NAMES: if self.base_path.endswith(n): @@ -99,11 +98,11 @@ def load(self, path): return ret -class HttpGetter(Getter): +class UrlGetter(Getter): """ default getter implementation for remote resource file """ def __init__(self, path): - super(HttpGetter, self).__init__(path) + super(UrlGetter, self).__init__(path) if self.base_path.endswith('/'): self.base_path = self.base_path[:-1] self.urls = [(path, '')] From 0cfbca04a08710ad057369a4a1d7298eab9fe205 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 15:48:50 +0800 Subject: [PATCH 03/21] add container_apply to simplify routine to handle ContainerType --- pyswagger/base.py | 93 +++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/pyswagger/base.py b/pyswagger/base.py index f982bcf..c98122b 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -3,6 +3,7 @@ import six import weakref import copy +import functools class ContainerType: @@ -18,13 +19,43 @@ class ContainerType: # dict of list container, like: {'xx': [], 'xx': [], ...} dict_of_list_ = 3 +def container_apply(ct, v, f, fd=None, fdl=None): + """ + """ + ret = None + if v == None: + return ret + + if ct == None: + ret = f(ct, v) + elif ct == ContainerType.list_: + ret = [] + for vv in v: + ret.append(f(ct, vv)) + elif ct == ContainerType.dict_: + ret = {} + for k, vv in six.iteritems(v): + ret[k] = fd(ct, vv, k) if fd else f(ct, vv) + elif ct == ContainerType.dict_of_list_: + ret = {} + for k, vv in six.iteritems(v): + if fdl: + fdl(ct, vv, k) + ret[k] = [] + for vvv in vv: + ret[k].append(fd(ct, vvv, k) if fd else f(ct, vvv)) + else: + raise ValueError('Unknown ContainerType: {0}'.format(ct)) + + return ret + class Context(object): """ Base of all parsing contexts """ # required fields, a list of strings __swagger_required__ = [] - + # parsing context of children fields, # a list of tuple (field-name, container-type, parsing-context) __swagger_child__ = [] @@ -95,12 +126,26 @@ def parse(self, obj=None): if not isinstance(obj, dict): raise ValueError('invalid obj passed: ' + str(type(obj))) + def _apply(x, kk, ct, v): + if key not in self._obj: + self._obj[kk] = {} if ct == None else [] + with x(self._obj, kk) as ctx: + ctx.parse(obj=v) + + def _apply_dict(x, kk, ct, v, k): + if k not in self._obj[kk]: + self._obj[kk][k] = {} if ct == ContainerType.dict_ else [] + with x(self._obj[kk], k) as ctx: + ctx.parse(obj=v) + + def _apply_dict_before_list(kk, ct, v, k): + self._obj[kk][k] = [] + if hasattr(self, '__swagger_child__'): # to nested objects for key, ct, ctx_kls in self.__swagger_child__: items = obj.get(key, None) - # make all containers to something not None if ct == ContainerType.list_: self._obj[key] = [] elif ct: @@ -109,26 +154,11 @@ def parse(self, obj=None): if items == None: continue - # deep into children - if ct == None: - self._obj[key] = {} - with ctx_kls(self._obj, key) as ctx: - ctx.parse(obj=items) - elif ct == ContainerType.list_: - for item in items: - with ctx_kls(self._obj, key) as ctx: - ctx.parse(obj=item) - elif ct == ContainerType.dict_: - for k, v in six.iteritems(items): - self._obj[key][k] = {} - with ctx_kls(self._obj[key], k) as ctx: - ctx.parse(obj=v) - elif ct == ContainerType.dict_of_list_: - for k, v in six.iteritems(items): - self._obj[key][k] = [] - for vv in v: - with ctx_kls(self._obj[key], k) as ctx: - ctx.parse(obj=vv) + container_apply(ct, items, + functools.partial(_apply, ctx_kls, key), + functools.partial(_apply_dict, ctx_kls, key), + functools.partial(_apply_dict_before_list, key) + ) # update _obj with obj if self._obj != None: @@ -173,7 +203,7 @@ def __init__(self, ctx): def _assign_parent(self, ctx): """ parent assignment, internal usage only """ - def _assign(cls, obj): + def _assign(cls, _, obj): if obj == None: return @@ -189,22 +219,7 @@ def _assign(cls, obj): if obj == None: continue - # iterate through children by ContainerType - if ct == None: - _assign(ctx, obj) - elif ct == ContainerType.list_: - for v in obj: - _assign(ctx, v) - elif ct == ContainerType.dict_: - for v in obj.values(): - _assign(ctx, v) - elif ct == ContainerType.dict_of_list_: - for v in obj.values(): - for vv in v: - _assign(ctx, vv) - else: - raise ValueError('Unknown ContainerType: {0}'.format(ct)) - + container_apply(ct, obj, functools.partial(_assign, ctx)) def get_private_name(self, f): """ get private protected name of an attribute From b8466faca951d772a5857f2f96fd7a051d754540 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 18:10:59 +0800 Subject: [PATCH 04/21] BaseObj.merge now deep copy everything --- pyswagger/base.py | 39 ++++++-- pyswagger/scanner/v2_0/resolve.py | 9 +- pyswagger/spec/v2_0/objects.py | 3 +- pyswagger/spec/v2_0/parser.py | 24 ++++- .../data/v2_0/resolve/path_item/swagger.json | 4 +- pyswagger/tests/test_base.py | 96 +++++++++++++++---- pyswagger/tests/v2_0/test_resolve.py | 4 +- 7 files changed, 138 insertions(+), 41 deletions(-) diff --git a/pyswagger/base.py b/pyswagger/base.py index c98122b..38f1982 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -1,9 +1,9 @@ from __future__ import absolute_import from .utils import jp_compose import six -import weakref import copy import functools +import weakref class ContainerType: @@ -260,19 +260,38 @@ def resolve(self, ts): return obj - def merge(self, other): + def merge(self, other, ctx): """ merge properties from other object, only merge from 'not None' to 'None'. """ - for name, _ in self.__swagger_fields__: + def _produce_new_obj(x, ct, v): + return x(None, None).produce().merge(v, x) + + for name, default in self.__swagger_fields__: v = getattr(other, name) - if v != None and getattr(self, name) == None: - if isinstance(v, weakref.ProxyTypes): - self.update_field(name, v) - elif isinstance(v, BaseObj): - self.update_field(name, weakref.proxy(v)) - else: - self.update_field(name, v) + if v == default or getattr(self, name) != default: + continue + + childs = [c for c in ctx.__swagger_child__ if c[0] == name] + if len(childs) == 0: + # we don't need to make a copy, + # since everything under SwaggerApp should be + # readonly. + self.update_field(name, v) + continue + + ct, cctx = childs[0][1], childs[0][2] + self.update_field(name, + container_apply( + ct, v, + functools.partial(_produce_new_obj, cctx) + )) + + # make sure parent is correctly assigned. + self._assign_parent(ctx) + + # allow cascade calling + return self @property def _parent_(self): diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index b55df17..0306775 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -1,5 +1,8 @@ from __future__ import absolute_import from ...scan import Dispatcher +from ...spec.v2_0.parser import ( + PathItemContext + ) from ...spec.v2_0.objects import ( Schema, Parameter, @@ -33,7 +36,7 @@ def _resolve(obj, app, prefix): obj.update_field('ref_obj', ro) -def _merge(obj, app, prefix): +def _merge(obj, app, prefix, ctx): """ resolve $ref as ref_obj, and merge ref_obj to self. This operation should be carried in a cascade manner. """ @@ -48,7 +51,7 @@ def _merge(obj, app, prefix): while (len(to_resolve)): o = to_resolve.pop() - o.merge(o.ref_obj) + o.merge(o.ref_obj, ctx) class Resolve(object): @@ -75,6 +78,6 @@ def _path_item(self, _, obj, app): # $ref in PathItem is 'merge', not 'replace' # we need to merge properties of others if missing # in current object. - _merge(obj, app, '#/paths') + _merge(obj, app, '#/paths', PathItemContext) diff --git a/pyswagger/spec/v2_0/objects.py b/pyswagger/spec/v2_0/objects.py index 5310fd5..43011b5 100644 --- a/pyswagger/spec/v2_0/objects.py +++ b/pyswagger/spec/v2_0/objects.py @@ -170,7 +170,8 @@ class Operation(six.with_metaclass(FieldMeta, BaseObj)): ('schemes', []), ('parameters', None), ('responses', None), - ('deprecated', None), + ('deprecated', False), + ('description', None), ('security', None), # for pyswagger diff --git a/pyswagger/spec/v2_0/parser.py b/pyswagger/spec/v2_0/parser.py index 8c677cf..4865292 100644 --- a/pyswagger/spec/v2_0/parser.py +++ b/pyswagger/spec/v2_0/parser.py @@ -1,5 +1,10 @@ from __future__ import absolute_import -from ...base import Context, ContainerType +from ...base import ( + Context, + ContainerType, + BaseObj, + NullContext + ) from .objects import ( Schema, Swagger, @@ -38,6 +43,14 @@ class AdditionalPropertiesContext(Context): """ Context of additionalProperties, """ + class _TmpObj(BaseObj): + def merge(self, other, _): + if isinstance(other, bool): + return other + + ret = Schema(NullContext()) + return ret.merge(other, SchemaContext) + @classmethod def is_produced(kls, obj): """ @@ -49,12 +62,17 @@ def is_produced(kls, obj): def produce(self): """ """ - return self._obj + if self._obj != None: + return self._obj + else: + return AdditionalPropertiesContext._TmpObj(self) def parse(self, obj=None): """ """ - if isinstance(obj, bool): + if obj == None: + self._obj = True + elif isinstance(obj, bool): self._obj = obj else: tmp = {'t': {}} diff --git a/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json b/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json index 8786d7b..77abed4 100644 --- a/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json +++ b/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json @@ -27,7 +27,7 @@ }, "/c":{ "put":{ - "operationId":"c.put", + "description":"c.put", "responses":{ "default":{ "description":"void" @@ -38,7 +38,7 @@ }, "/d":{ "post":{ - "operationId":"d.post", + "description":"d.post", "responses":{ "default":{ "description":"void" diff --git a/pyswagger/tests/test_base.py b/pyswagger/tests/test_base.py index ac4eca1..e08b5ee 100644 --- a/pyswagger/tests/test_base.py +++ b/pyswagger/tests/test_base.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from pyswagger import base import unittest -import weakref import six @@ -87,32 +86,89 @@ def test_field_default_value(self): def test_merge(self): """ test merge function """ - tmp = {'t': {}} - obj1 = {'a': [{}, {}, {}], 'd': {}, 'f': ''} - obj2 = {'a': [{}]} - o3 = TestObj(base.NullContext()) - with TestContext(tmp, 't') as ctx: + class MergeObj(six.with_metaclass(base.FieldMeta, base.BaseObj)): + __swagger_fields__ = [ + ('ma', None), + ('mb', None), + ('mc', {}) + ] + + class MergeContext(base.Context): + __swagger_child__ = [ + ('ma', None, TestContext), + ('mb', None, TestContext), + ('mc', base.ContainerType.dict_, TestContext) + ] + __swagger_ref_object__ = MergeObj + + + tmp = {'t': {}} + obj2 = {'mb':{'a':[{}, {}, {}]}} + obj1 = { + 'ma':{'a':[{}, {}, {}, {}]}, + 'mb':{'a':[{}, {}]}, + 'mc':{'/a': {'a': [{}], 'b': {'bb': {}}, 'c': {'cc': [{}]}, 'd': {}}} + } + o3 = MergeObj(base.NullContext()) + + with MergeContext(tmp, 't') as ctx: ctx.parse(obj1) o1 = tmp['t'] - with TestContext(tmp, 't') as ctx: + with MergeContext(tmp, 't') as ctx: ctx.parse(obj2) o2 = tmp['t'] - self.assertTrue(len(o2.a), 1) - self.assertEqual(o2.d, None) - self.assertEqual(o2.f, None) - - o2.merge(o1) - self.assertTrue(len(o2.a), 1) - self.assertEqual(o2.f, '') - self.assertTrue(isinstance(o2.d, ChildObj)) - self.assertTrue(isinstance(o2.d, weakref.ProxyTypes)) - - o3.merge(o2) - self.assertEqual(id(o2.d), id(o3.d)) - self.assertTrue(isinstance(o3.d, weakref.ProxyTypes)) + def _chk(o_from, o_to): + # existing children are not affected + self.assertTrue(len(o_to.mb.a), 3) + # non-existing children are fully copied3 + self.assertEqual(len(o_to.ma.a), 4) + self.assertNotEqual(id(o_to.ma), id(o_from.ma)) + # make sure complex children are copied + self.assertNotEqual(id(o_to.mc), id(o_from.mc)) + self.assertEqual(len(o_to.mc['/a'].a), 1) + self.assertTrue(isinstance(o_to.mc['/a'].b['bb'], ChildObj)) + self.assertNotEqual(id(o_to.mc['/a'].b['bb']), id(o_from.mc['/a'].b['bb'])) + self.assertTrue(isinstance(o_to.mc['/a'].c['cc'][0], ChildObj)) + self.assertNotEqual(id(o_to.mc['/a'].c['cc'][0]), id(o_from.mc['/a'].c['cc'][0])) + self.assertTrue(o_to.mc['/a'].d, ChildObj) + self.assertNotEqual(id(o_to.mc['/a'].d), id(o1.mc['/a'].d)) + + def _chk_parent(o_from, o_to): + for v in o_to.ma.a: + self.assertEqual(id(v._parent_), id(o_to.ma)) + self.assertNotEqual(id(v._parent_), id(o_from.ma)) + + self.assertEqual(id(o_to.ma._parent_), id(o_to)) + self.assertEqual(id(o_to.mb._parent_), id(o_to)) + self.assertEqual(id(o_to.mc['/a']._parent_), id(o_to)) + self.assertEqual(id(o_to.mc['/a'].a[0]._parent_), id(o_to.mc['/a'])) + self.assertEqual(id(o_to.mc['/a'].b['bb']._parent_), id(o_to.mc['/a'])) + self.assertEqual(id(o_to.mc['/a'].c['cc'][0]._parent_), id(o_to.mc['/a'])) + + self.assertEqual(o2.ma, None) + self.assertTrue(isinstance(o2.mb, TestObj)) + self.assertTrue(len(o2.mb.a), 3) + self.assertEqual(len(o2.mc), 0) + + id_mb = id(o2.mb) + + o2.merge(o1, MergeContext) + self.assertNotEqual(id(o2.mb), id(o1.mb)) + self.assertEqual(id(o2.mb), id_mb) + + # cascade merge + o3.merge(o2, MergeContext) + + _chk(o1, o2) + _chk(o2, o3) + _chk(o1, o3) + + _chk_parent(o1, o2) + _chk_parent(o2, o3) + _chk_parent(o1, o3) def test_resolve(self): """ test resolve function """ diff --git a/pyswagger/tests/v2_0/test_resolve.py b/pyswagger/tests/v2_0/test_resolve.py index 5885ce2..5a2ce8c 100644 --- a/pyswagger/tests/v2_0/test_resolve.py +++ b/pyswagger/tests/v2_0/test_resolve.py @@ -21,8 +21,8 @@ def test_path_item(self): self.assertTrue(isinstance(a, objects.PathItem)) self.assertTrue(a.get.operationId, 'a.get') - self.assertTrue(a.put.operationId, 'c.put') - self.assertTrue(a.post.operationId, 'd.post') + self.assertTrue(a.put.description, 'c.put') + self.assertTrue(a.post.description, 'd.post') class ResolveTestCase(unittest.TestCase): From 5c6ec2ff6f978012500e55e0fef8ca6bfe3ae5fe Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 18:39:13 +0800 Subject: [PATCH 05/21] deref of cascade $ref --- .../data/v2_0/resolve/deref/swagger.json | 22 +++++++++++++++++++ pyswagger/tests/v2_0/test_resolve.py | 16 ++++++++++++++ pyswagger/utils.py | 6 +++-- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 pyswagger/tests/data/v2_0/resolve/deref/swagger.json diff --git a/pyswagger/tests/data/v2_0/resolve/deref/swagger.json b/pyswagger/tests/data/v2_0/resolve/deref/swagger.json new file mode 100644 index 0000000..5e7d169 --- /dev/null +++ b/pyswagger/tests/data/v2_0/resolve/deref/swagger.json @@ -0,0 +1,22 @@ +{ + "swagger":"2.0", + "host":"test.com", + "basePath":"/v1", + "paths":{ + + }, + "definitions":{ + "s1":{ + "$ref":"#/definitions/s2" + }, + "s2":{ + "$ref":"#/definitions/s3" + }, + "s3":{ + "$ref":"#/definitions/s4" + }, + "s4":{ + "type":"string" + } + } +} \ No newline at end of file diff --git a/pyswagger/tests/v2_0/test_resolve.py b/pyswagger/tests/v2_0/test_resolve.py index 5a2ce8c..87f82da 100644 --- a/pyswagger/tests/v2_0/test_resolve.py +++ b/pyswagger/tests/v2_0/test_resolve.py @@ -58,3 +58,19 @@ def test_raises(self): self.assertRaises(ValueError, self.app.resolve, None) self.assertRaises(ValueError, self.app.resolve, '') + +class DerefTestCase(unittest.TestCase): + """ test for pyswagger.utils.deref """ + + @classmethod + def setUpClass(kls): + kls.app = SwaggerApp._create_(get_test_data_folder( + version='2.0', + which=os.path.join('resolve', 'deref') + )) + + def test_deref(self): + od = utils.deref(self.app.resolve('#/definitions/s1')) + + self.assertEqual(id(od), id(self.app.resolve('#/definitions/s4'))) + diff --git a/pyswagger/utils.py b/pyswagger/utils.py index e575b8f..b1af882 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -216,8 +216,10 @@ def _decode(s): return [_decode(ss) for ss in s.split('/')] def deref(obj): - o = getattr(obj, 'ref_obj', None) if obj else None - return o if o else obj + cur = obj + while cur and getattr(cur, 'ref_obj', None) != None: + cur = cur.ref_obj + return cur if cur else obj def get_dict_as_tuple(d): """ get the first item in dict, From 208b0b04cc95418ff5cacb52576b20a3dc42e1d6 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 18:44:44 +0800 Subject: [PATCH 06/21] more test conditions --- pyswagger/tests/v2_0/test_resolve.py | 9 +++++++-- pyswagger/utils.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyswagger/tests/v2_0/test_resolve.py b/pyswagger/tests/v2_0/test_resolve.py index 87f82da..02be55d 100644 --- a/pyswagger/tests/v2_0/test_resolve.py +++ b/pyswagger/tests/v2_0/test_resolve.py @@ -17,13 +17,18 @@ def setUpClass(kls): def test_path_item(self): """ make sure PathItem is correctly merged """ - a = self.app.resolve(utils.jp_compose('/a', '#/paths')) + a = self.app.resolve(utils.jp_compose('/a', '#/paths')) - self.assertTrue(isinstance(a, objects.PathItem)) + self.assertTrue(isinstance(a, objects.PathItem)) self.assertTrue(a.get.operationId, 'a.get') self.assertTrue(a.put.description, 'c.put') self.assertTrue(a.post.description, 'd.post') + b = self.app.resolve(utils.jp_compose('/b', '#/paths')) + self.assertTrue(b.get.operationId, 'b.get') + self.assertTrue(b.put.description, 'c.put') + self.assertTrue(b.post.description, 'd.post') + class ResolveTestCase(unittest.TestCase): """ test for $ref other than PathItem """ diff --git a/pyswagger/utils.py b/pyswagger/utils.py index b1af882..213a5e9 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -216,6 +216,8 @@ def _decode(s): return [_decode(ss) for ss in s.split('/')] def deref(obj): + """ dereference $ref + """ cur = obj while cur and getattr(cur, 'ref_obj', None) != None: cur = cur.ref_obj From 668526a49c4900c27ba0050ed6c4dd2603e0efd2 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 18:46:11 +0800 Subject: [PATCH 07/21] prepend / for shortcut --- pyswagger/core.py | 2 +- pyswagger/tests/v2_0/test_op_access.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyswagger/core.py b/pyswagger/core.py index 3c38c38..ac2a242 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -318,7 +318,7 @@ def resolve(self, path): def s(self, p, b=_shortcut_[sc_path]): """ shortcut to access Objects """ - return self.resolve(utils.jp_compose(p, base=b)) + return self.resolve(utils.jp_compose('/' + p if not p.startswith('/') else p, base=b)) class SwaggerSecurity(object): diff --git a/pyswagger/tests/v2_0/test_op_access.py b/pyswagger/tests/v2_0/test_op_access.py index be62234..86808c8 100644 --- a/pyswagger/tests/v2_0/test_op_access.py +++ b/pyswagger/tests/v2_0/test_op_access.py @@ -36,4 +36,6 @@ def test_shortcut(self): """ _check(self, self.app.s('/pet').post) _check(self, self.app.s('/pet', b=SwaggerApp._shortcut_[SwaggerApp.sc_path]).post) + _check(self, self.app.s('pet').post) + _check(self, self.app.s('pet', b=SwaggerApp._shortcut_[SwaggerApp.sc_path]).post) From 81940de80218ebc6e4dc4999438b0f177bb3fe30 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Tue, 9 Dec 2014 21:16:23 +0800 Subject: [PATCH 08/21] remove tailing space --- pyswagger/tests/v2_0/test_resolve.py | 36 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyswagger/tests/v2_0/test_resolve.py b/pyswagger/tests/v2_0/test_resolve.py index 02be55d..f76d574 100644 --- a/pyswagger/tests/v2_0/test_resolve.py +++ b/pyswagger/tests/v2_0/test_resolve.py @@ -1,21 +1,21 @@ -from pyswagger import SwaggerApp, utils -from pyswagger.spec.v2_0 import objects -from ..utils import get_test_data_folder -import unittest -import os +from pyswagger import SwaggerApp, utils +from pyswagger.spec.v2_0 import objects +from ..utils import get_test_data_folder +import unittest +import os -class ResolvePathItemTestCase(unittest.TestCase): - """ test for PathItem $ref """ +class ResolvePathItemTestCase(unittest.TestCase): + """ test for PathItem $ref """ - @classmethod - def setUpClass(kls): - kls.app = SwaggerApp._create_(get_test_data_folder( - version='2.0', - which=os.path.join('resolve', 'path_item') - )) + @classmethod + def setUpClass(kls): + kls.app = SwaggerApp._create_(get_test_data_folder( + version='2.0', + which=os.path.join('resolve', 'path_item') + )) - def test_path_item(self): + def test_path_item(self): """ make sure PathItem is correctly merged """ a = self.app.resolve(utils.jp_compose('/a', '#/paths')) @@ -35,10 +35,10 @@ class ResolveTestCase(unittest.TestCase): @classmethod def setUpClass(kls): - kls.app = SwaggerApp._create_(get_test_data_folder( - version='2.0', - which=os.path.join('resolve', 'other') - )) + kls.app = SwaggerApp._create_(get_test_data_folder( + version='2.0', + which=os.path.join('resolve', 'other') + )) def test_schema(self): """ make sure $ref to Schema works """ From b909c347aca586437d371884ea37613e6c40f252 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 11 Dec 2014 02:33:54 +0800 Subject: [PATCH 09/21] DFS circle detection --- pyswagger/tests/test_utils.py | 118 ++++++++++++++++++++++++++++++++++ pyswagger/utils.py | 34 ++++++++++ 2 files changed, 152 insertions(+) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index f2e7829..6872fc0 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -1,6 +1,7 @@ from pyswagger import utils from datetime import datetime import unittest +import functools class SwaggerUtilsTestCase(unittest.TestCase): @@ -93,3 +94,120 @@ def test_path2url(self): """ test path2url """ self.assertEqual(utils.path2url('/opt/local/a.json'), 'file:///opt/local/a.json') + +class WalkTestCase(unittest.TestCase): + """ test for walk """ + + @staticmethod + def _out(conf, idx): + return conf[idx] + + def test_self_cycle(self): + conf = { + 0: [0] + } + + cyc, _ = utils.walk( + 0, functools.partial(WalkTestCase._out, conf) + ) + self.assertEqual(cyc, [[0, 0]]) + + def test_1_long_cycle(self): + conf = { + 0: [1], + 1: [2], + 2: [3], + 3: [4], + 4: [5], + 5: [1] + } + + cyc = [] + visited = [] + for i in range(6): + _cyc, visited = utils.walk( + i, + functools.partial(WalkTestCase._out, conf), + visited + ) + cyc.extend(_cyc) + + self.assertEqual(cyc, [[1, 2, 3, 4, 5, 1]]) + + def test_multiple_cycles(self): + conf = { + 0: [6], + 1: [6], + 2: [0], + 3: [1], + 4: [4], + 5: [3], + 6: [3], + 7: [4], + 8: [0] + } + + cyc = [] + visited = [] + for i in range(9): + _cyc, visited = utils.walk( + i, + functools.partial(WalkTestCase._out, conf), + visited + ) + cyc.extend(_cyc) + + self.assertEqual(cyc, [ + [6, 3, 1, 6], + [4, 4] + ]) + + def test_cycles_share_border(self): + conf = { + 0: [1], + 1: [2], + 2: [3], + 3: [0, 5], + 4: [2], + 5: [4] + } + + cyc = [] + visited = [] + for i in range(6): + _cyc, visited = utils.walk( + i, + functools.partial(WalkTestCase._out, conf), + visited + ) + cyc.extend(_cyc) + + self.assertEqual(cyc, [ + [0, 1, 2, 3, 0], + [2, 3, 5, 4, 2] + ]) + + def test_no_cycle(self): + conf = { + 0: [1, 2], + 1: [2, 3], + 2: [3, 4], + 3: [4, 5], + 4: [5, 6], + 5: [6, 7], + 6: [7], + 7: [] + } + + cyc = [] + visited = [] + for i in range(8): + _cyc, visited = utils.walk( + i, + functools.partial(WalkTestCase._out, conf), + visited + ) + cyc.extend(_cyc) + + self.assertEqual(cyc, []) + diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 213a5e9..09fc1f2 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -260,3 +260,37 @@ def get_swagger_version(obj): # should be an instance of BaseObj return obj.swaggerVersion if hasattr(obj, 'swaggerVersion') else obj.swagger +def walk(start, ofn, visited=None): + """ Non recursive DFS to detect cycles + """ + visited = visited if visited else [] + ctx = {} + stk = [] + stk.append(start) + + cyc = [] + while len(stk): + top = stk[-1] + + if top in visited: + stk.pop() + continue + + if top not in ctx: + ctx.update({top:list(set(ofn(top)))}) + + ctx[top] = [v for v in ctx[top] if v not in visited] + + if len(ctx[top]): + n = ctx[top][0] + if n in stk: + cyc.append(stk[stk.index(n):]+[n]) + ctx[top].pop(0) + else: + stk.append(n) + else: + visited.append(top) + stk.pop() + + return cyc, visited + From 2cf6fb33648c19bb3c6da1b4d8d19b0e7679040c Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 11 Dec 2014 10:34:23 +0800 Subject: [PATCH 10/21] fix cycle detection --- pyswagger/tests/test_utils.py | 56 ++++++++++++++++++++++++----------- pyswagger/utils.py | 36 ++++++++++++---------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 6872fc0..72f3cb5 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -107,7 +107,7 @@ def test_self_cycle(self): 0: [0] } - cyc, _ = utils.walk( + cyc = utils.walk( 0, functools.partial(WalkTestCase._out, conf) ) self.assertEqual(cyc, [[0, 0]]) @@ -123,14 +123,12 @@ def test_1_long_cycle(self): } cyc = [] - visited = [] for i in range(6): - _cyc, visited = utils.walk( + cyc = utils.walk( i, functools.partial(WalkTestCase._out, conf), - visited + cyc ) - cyc.extend(_cyc) self.assertEqual(cyc, [[1, 2, 3, 4, 5, 1]]) @@ -148,17 +146,15 @@ def test_multiple_cycles(self): } cyc = [] - visited = [] for i in range(9): - _cyc, visited = utils.walk( + cyc = utils.walk( i, functools.partial(WalkTestCase._out, conf), - visited + cyc ) - cyc.extend(_cyc) self.assertEqual(cyc, [ - [6, 3, 1, 6], + [1, 6, 3, 1], [4, 4] ]) @@ -173,14 +169,12 @@ def test_cycles_share_border(self): } cyc = [] - visited = [] for i in range(6): - _cyc, visited = utils.walk( + cyc = utils.walk( i, functools.partial(WalkTestCase._out, conf), - visited + cyc ) - cyc.extend(_cyc) self.assertEqual(cyc, [ [0, 1, 2, 3, 0], @@ -200,14 +194,40 @@ def test_no_cycle(self): } cyc = [] - visited = [] for i in range(8): - _cyc, visited = utils.walk( + cyc = utils.walk( i, functools.partial(WalkTestCase._out, conf), - visited + cyc ) - cyc.extend(_cyc) self.assertEqual(cyc, []) + def test_multiple_cycles_2(self): + conf = { + 0: [1, 4], + 1: [2], + 2: [0, 3], + 3: [4, 5], + 4: [1, 2], + 5: [4] + } + + cyc = [] + for i in range(6): + cyc = utils.walk( + i, + functools.partial(WalkTestCase._out, conf), + cyc + ) + + self.assertEqual(sorted(cyc), sorted([ + [0, 1, 2, 0], + [0, 4, 1, 2, 0], + [0, 4, 2, 0], + [1, 2, 3, 4, 1], + [1, 2, 3, 5, 4, 1], + [2, 3, 5, 4, 2], + [2, 3 ,4, 2] + ])) + diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 09fc1f2..85c77e5 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -260,37 +260,41 @@ def get_swagger_version(obj): # should be an instance of BaseObj return obj.swaggerVersion if hasattr(obj, 'swaggerVersion') else obj.swagger -def walk(start, ofn, visited=None): +def walk(start, ofn, cyc=None): """ Non recursive DFS to detect cycles + + :param start: start vertex in graph + :param ofn: function to get the list of outgoing edges of a vertex + :param cyc: list of existing cycles, cycles are represented in a list started with minimum vertex. + :return: cycles + :rtype: list of lists """ - visited = visited if visited else [] - ctx = {} - stk = [] - stk.append(start) + ctx, stk = {}, [start] + cyc = [] if cyc == None else cyc - cyc = [] while len(stk): top = stk[-1] - if top in visited: - stk.pop() - continue - if top not in ctx: - ctx.update({top:list(set(ofn(top)))}) - - ctx[top] = [v for v in ctx[top] if v not in visited] + ctx.update({top:list(ofn(top))}) if len(ctx[top]): n = ctx[top][0] if n in stk: - cyc.append(stk[stk.index(n):]+[n]) + nc = stk[stk.index(n):] + ni = nc.index(min(nc)) + nc = nc[ni:] + nc[:ni] + [min(nc)] + if nc not in cyc: + cyc.append(nc) + ctx[top].pop(0) else: stk.append(n) else: - visited.append(top) + ctx.pop(top) stk.pop() + if len(stk): + ctx[stk[-1]].remove(top) - return cyc, visited + return cyc From a7669cb5d168064cf917003a40f64de01569a8ae Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 11 Dec 2014 12:31:05 +0800 Subject: [PATCH 11/21] cycle detection in SwaggerApp.prepare - when upgrade from 1.2, now skip basePath part, everything after host would be used as path. --- pyswagger/core.py | 23 +++-- pyswagger/scanner/__init__.py | 1 + pyswagger/scanner/cycle_detector.py | 98 +++++++++++++++++++ pyswagger/scanner/v1_2/upgrade.py | 5 +- .../data/v2_0/circular/path_item/swagger.json | 19 ++++ .../data/v2_0/circular/schema/swagger.json | 82 ++++++++++++++++ pyswagger/tests/v1_2/test_app.py | 4 +- pyswagger/tests/v1_2/test_upgrade.py | 50 ++++++---- pyswagger/tests/v2_0/test_circular.py | 56 +++++++++++ 9 files changed, 306 insertions(+), 32 deletions(-) create mode 100644 pyswagger/scanner/cycle_detector.py create mode 100644 pyswagger/tests/data/v2_0/circular/path_item/swagger.json create mode 100644 pyswagger/tests/data/v2_0/circular/schema/swagger.json create mode 100644 pyswagger/tests/v2_0/test_circular.py diff --git a/pyswagger/core.py b/pyswagger/core.py index ac2a242..2b59d83 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -3,7 +3,7 @@ from .spec.v1_2.parser import ResourceListContext from .spec.v2_0.parser import SwaggerContext from .scan import Scanner -from .scanner import TypeReduce +from .scanner import TypeReduce, CycleDetector from .scanner.v1_2 import Upgrade from .scanner.v2_0 import AssignParent, Resolve, PatchObject from pyswagger import utils @@ -179,11 +179,11 @@ def __validate(self, url=None): return v.errs - def __prepare_obj(self, url): + def __prepare_obj(self, url, strict): """ """ s = Scanner(self) - self.__validate(url=url) + self.validate(url=url, strict=strict) obj = self.__url2obj[url] if self.version == '1.2': @@ -235,39 +235,44 @@ def load(kls, url, getter=None): return app - def validate(self, strict=True): + def validate(self, url=None, strict=True): """ check if this Swagger API valid or not. :param bool strict: when in strict mode, exception would be raised if not valid. :return: validation errors :rtype: list of tuple(where, type, msg). """ - errs = self.__validate() + errs = self.__validate(url) if strict and len(errs): raise ValueError('this Swagger App contains error: {0}.'.format(len(errs))) return errs - def prepare(self): + def prepare(self, strict=True): """ preparation for loaded json """ - self.__prepare_obj(url=self.__root_url) + self.__prepare_obj(url=self.__root_url, strict=strict) self.__root = self.__url2obj[self.__root_url] s = Scanner(self) # reducer for Operation tr = TypeReduce() - s.scan(root=self.__root, route=[tr]) + cy = CycleDetector() + s.scan(root=self.__root, route=[tr, cy]) # 'op' -- shortcut for Operation with tag and operaionId self.__op = utils.ScopeDict(tr.op) # 'm' -- shortcut for model in Swagger 1.2 self.__m = utils.ScopeDict(self.__root.definitions) + # cycle detection + if len(cy.cycles['schema']) > 0 and strict: + raise ValueError('Cycles detected in Schema Object: {0}'.format(cy.cycles['schema'])) + @classmethod - def create(kls, url, getter=None): + def create(kls, url, strict=True, getter=None): """ factory of SwaggerApp :param str url: url of path of Swagger API definition diff --git a/pyswagger/scanner/__init__.py b/pyswagger/scanner/__init__.py index 89f16fb..079717d 100644 --- a/pyswagger/scanner/__init__.py +++ b/pyswagger/scanner/__init__.py @@ -1 +1,2 @@ from .type_reducer import TypeReduce +from .cycle_detector import CycleDetector diff --git a/pyswagger/scanner/cycle_detector.py b/pyswagger/scanner/cycle_detector.py new file mode 100644 index 0000000..a8984b6 --- /dev/null +++ b/pyswagger/scanner/cycle_detector.py @@ -0,0 +1,98 @@ +from __future__ import absolute_import +from ..utils import jp_compose, walk +from ..scan import Dispatcher +from ..spec.v2_0.objects import ( + Schema, + Parameter, + Response, + PathItem, + ) +import functools +import six + +def _out(app, prefix, path): + try: + obj = app.resolve(path) + except: + obj = app.resolve(jp_compose(path, base=prefix)) + # exception would be raised when unable to resolve + + r = getattr(obj, '$ref') + return [r] if r else [] + +def _schema_out_obj(obj, out=None): + out = [] if out == None else out + + for o in six.itervalues(obj.properties): + out = _schema_out_obj(o, out) + + for o in obj.allOf: + out = _schema_out_obj(o, out) + + if isinstance(obj.additionalProperties, Schema): + out = _schema_out_obj(obj.additionalProperties, out) + + if obj.items: + out = _schema_out_obj(obj.items, out) + + r = getattr(obj, '$ref') + if r: + out.append(r) + + return out + +def _schema_out(app, path): + try: + obj = app.resolve(path) + except: + obj = app.resolve(jp_compose(path, base='#/definitions')) + # exception would be raised when unable to resolve + + return [] if obj == None else _schema_out_obj(obj) + + +class CycleDetector(object): + """ circular detector """ + + class Disp(Dispatcher): pass + + def __init__(self): + self.cycles = { + 'schema':[], + 'parameter':[], + 'response':[], + 'path_item':[] + } + + @Disp.register([Schema]) + def _schema(self, path, _, app): + self.cycles['schema'] = walk( + path, + functools.partial(_schema_out, app), + self.cycles['schema'] + ) + + @Disp.register([Parameter]) + def _parameter(self, path, _, app): + self.cycles['parameter'] = walk( + path, + functools.partial(_out, app, '#/parameters'), + self.cycles['parameter'] + ) + + @Disp.register([Response]) + def _response(self, path, _, app): + self.cycles['response'] = walk( + path, + functools.partial(_out, app, '#/responses'), + self.cycles['response'] + ) + + @Disp.register([PathItem]) + def _path_item(self, path, _, app): + self.cycles['path_item'] = walk( + path, + functools.partial(_out, app, '#/paths'), + self.cycles['path_item'] + ) + diff --git a/pyswagger/scanner/v1_2/upgrade.py b/pyswagger/scanner/v1_2/upgrade.py index 15ccf8c..1b5bf69 100644 --- a/pyswagger/scanner/v1_2/upgrade.py +++ b/pyswagger/scanner/v1_2/upgrade.py @@ -279,11 +279,12 @@ def swagger(self): if len(common_path) > 0: p = six.moves.urllib.parse.urlparse(common_path) self.__swagger.update_field('host', p.netloc) - self.__swagger.update_field('basePath', p.path) + new_common_path = six.moves.urllib.parse.urlunparse(( + p.scheme, p.netloc, '', '', '', '')) new_path = {} for k in self.__swagger.paths.keys(): - new_path[k[len(common_path):]] = self.__swagger.paths[k] + new_path[k[len(new_common_path):]] = self.__swagger.paths[k] self.__swagger.update_field('paths', new_path) return self.__swagger diff --git a/pyswagger/tests/data/v2_0/circular/path_item/swagger.json b/pyswagger/tests/data/v2_0/circular/path_item/swagger.json new file mode 100644 index 0000000..3c838df --- /dev/null +++ b/pyswagger/tests/data/v2_0/circular/path_item/swagger.json @@ -0,0 +1,19 @@ +{ + "swagger":"2.0", + "host":"test.com", + "basePath":"/v1", + "paths":{ + "/p1":{ + "$ref":"#/paths/~1p2" + }, + "/p2":{ + "$ref":"#/paths/~1p3" + }, + "/p3":{ + "$ref":"#/paths/~1p4" + }, + "/p4":{ + "$ref":"#/paths/~1p1" + } + } +} \ No newline at end of file diff --git a/pyswagger/tests/data/v2_0/circular/schema/swagger.json b/pyswagger/tests/data/v2_0/circular/schema/swagger.json new file mode 100644 index 0000000..abb6d97 --- /dev/null +++ b/pyswagger/tests/data/v2_0/circular/schema/swagger.json @@ -0,0 +1,82 @@ +{ + "swagger":"2.0", + "host":"test.com", + "basePath":"/v1", + "paths":{ + + }, + "definitions":{ + "s1":{ + "$ref":"#/definitions/s2" + }, + "s2":{ + "$ref":"#/definitions/s3" + }, + "s3":{ + "$ref":"#/definitions/s4" + }, + "s4":{ + "$ref":"#/definitions/s1" + }, + "s5":{ + "$ref":"#/definitions/s5" + }, + "s6":{ + "type":"array", + "items":{ + "type":"array", + "items":{ + "$ref":"#/definitions/s7" + } + } + }, + "s7":{ + "$ref":"#/definitions/s6" + }, + "s8":{ + "$ref":"#/definitions/s5" + }, + "s9":{ + "allOf":[ + { + "$ref":"#/definitions/s10" + } + ] + }, + "s10":{ + "type":"array", + "items":{ + "$ref":"#/definitions/s11" + } + }, + "s11":{ + "allOf":[ + { + "$ref":"#/definitions/s9" + } + ] + }, + "s12":{ + "properties":{ + "id":{ + "$ref":"#/definitions/s13" + } + } + }, + "s13":{ + "properties":{ + "name":{ + "$ref":"#/definitions/s12" + } + } + }, + "s14":{ + "additionalProperties":{ + "$ref":"#/definitions/s15" + } + }, + "s15":{ + "$ref":"#/definitions/s14" + } + } +} \ No newline at end of file diff --git a/pyswagger/tests/v1_2/test_app.py b/pyswagger/tests/v1_2/test_app.py index 81d282c..4e9010b 100644 --- a/pyswagger/tests/v1_2/test_app.py +++ b/pyswagger/tests/v1_2/test_app.py @@ -128,8 +128,8 @@ def test_ref(self): self.assertRaises(ValueError, self.app.resolve, '//') self.assertTrue(isinstance(self.app.resolve('#/definitions/user!##!User'), Schema)) - self.assertTrue(isinstance(self.app.resolve('#/paths/~1user~1{username}/put'), Operation)) - self.assertEqual(self.app.resolve('#/paths/~1store~1order/post/produces'), ['application/json']) + self.assertTrue(isinstance(self.app.resolve('#/paths/~1api~1user~1{username}/put'), Operation)) + self.assertEqual(self.app.resolve('#/paths/~1api~1store~1order/post/produces'), ['application/json']) self.assertEqual(self.app.resolve('#/host'), 'petstore.swagger.wordnik.com') def test_scope_dict(self): diff --git a/pyswagger/tests/v1_2/test_upgrade.py b/pyswagger/tests/v1_2/test_upgrade.py index 9f9c3de..5e07119 100644 --- a/pyswagger/tests/v1_2/test_upgrade.py +++ b/pyswagger/tests/v1_2/test_upgrade.py @@ -18,7 +18,7 @@ def test_resource_list(self): self.assertEqual(s.swagger, '2.0') self.assertEqual(s.host, 'petstore.swagger.wordnik.com') - self.assertEqual(s.basePath, '/api') + self.assertEqual(s.basePath, '') self.assertEqual(s.info.version, '1.0.0') self.assertEqual(s.schemes, ['http', 'https']) self.assertEqual(s.consumes, []) @@ -34,25 +34,25 @@ def test_resource(self): p = self.app.root.paths self.assertEqual(sorted(p.keys()), sorted([ - '/user/createWithArray', - '/store/order', - '/user/login', - '/user', - '/pet', - '/pet/findByTags', - '/pet/findByStatus', - '/store/order/{orderId}', - '/user/logout', - '/pet/uploadImage', - '/user/createWithList', - '/user/{username}', - '/pet/{petId}' + '/api/user/createWithArray', + '/api/store/order', + '/api/user/login', + '/api/user', + '/api/pet', + '/api/pet/findByTags', + '/api/pet/findByStatus', + '/api/store/order/{orderId}', + '/api/user/logout', + '/api/pet/uploadImage', + '/api/user/createWithList', + '/api/user/{username}', + '/api/pet/{petId}' ])) def test_operation(self): """ Operation -> Operation """ - p = self.app.root.paths['/pet/{petId}'] + p = self.app.root.paths['/api/pet/{petId}'] # getPetById o = p.get @@ -75,7 +75,7 @@ def test_operation(self): self.assertEqual(getattr(r.schema.items, '$ref'), '#/definitions/pet!##!Pet') # createUser - o = self.app.root.paths['/user'].post + o = self.app.root.paths['/api/user'].post self.assertEqual(o.tags, ['user']) self.assertEqual(o.operationId, 'createUser') @@ -98,20 +98,20 @@ def test_parameter(self): """ Parameter -> Parameter """ # body - o = self.app.root.paths['/pet/{petId}'].patch + o = self.app.root.paths['/api/pet/{petId}'].patch p = [p for p in o.parameters if getattr(p, 'in') == 'body'][0] self.assertEqual(getattr(p, 'in'), 'body') self.assertEqual(p.required, True) self.assertEqual(getattr(p.schema, '$ref'), '#/definitions/pet!##!Pet') # form - o = self.app.root.paths['/pet/uploadImage'].post + o = self.app.root.paths['/api/pet/uploadImage'].post p = [p for p in o.parameters if getattr(p, 'in') == 'formData' and p.type == 'string'][0] self.assertEqual(p.name, 'additionalMetadata') self.assertEqual(p.required, False) # file - o = self.app.root.paths['/pet/uploadImage'].post + o = self.app.root.paths['/api/pet/uploadImage'].post p = [p for p in o.parameters if getattr(p, 'in') == 'formData' and p.type == 'file'][0] self.assertEqual(p.name, 'file') self.assertEqual(p.required, False) @@ -181,3 +181,15 @@ def test_item(self): else: self.fail('ValueError not raised') + +class ModelSubtypesTestCase(unittest.TestCase): + """ test for upgrade /data/v1_2/model_subtypes """ + + @classmethod + def setUpClass(kls): + kls.app = SwaggerApp._create_(get_test_data_folder(version='1.2', which='model_subtypes')) + + def test_path_item(self): + paths = self.app.resolve('#/paths') + self.assertEqual(sorted(list(paths.keys())), sorted(['/api/user', '/api/user/{username}'])) + diff --git a/pyswagger/tests/v2_0/test_circular.py b/pyswagger/tests/v2_0/test_circular.py new file mode 100644 index 0000000..0008880 --- /dev/null +++ b/pyswagger/tests/v2_0/test_circular.py @@ -0,0 +1,56 @@ +from pyswagger import SwaggerApp +from ..utils import get_test_data_folder +from ...scanner import CycleDetector +from ...scan import Scanner +import unittest +import os + + +class CircularRefTestCase(unittest.TestCase): + """ test for circular reference guard """ + + def test_path_item_prepare_with_cycle(self): + app = SwaggerApp.load(get_test_data_folder( + version='2.0', + which=os.path.join('circular', 'path_item') + )) + + # should raise nothing + app.prepare() + + def test_path_item(self): + app = SwaggerApp.create(get_test_data_folder( + version='2.0', + which=os.path.join('circular', 'path_item') + )) + s = Scanner(app) + c = CycleDetector() + s.scan(root=app.raw, route=[c]) + self.assertEqual(sorted(c.cycles['path_item']), sorted([[ + '#/paths/~1p1', + '#/paths/~1p2', + '#/paths/~1p3', + '#/paths/~1p4', + '#/paths/~1p1' + ]])) + + def test_schema(self): + app = SwaggerApp.load(get_test_data_folder( + version='2.0', + which=os.path.join('circular', 'schema') + )) + app.prepare(strict=False) + + s = Scanner(app) + c = CycleDetector() + s.scan(root=app.raw, route=[c]) + self.maxDiff = None + self.assertEqual(sorted(c.cycles['schema']), sorted([ + ['#/definitions/s10', '#/definitions/s11', '#/definitions/s9', '#/definitions/s10'], + ['#/definitions/s5', '#/definitions/s5'], + ['#/definitions/s1', '#/definitions/s2', '#/definitions/s3', '#/definitions/s4', '#/definitions/s1'], + ['#/definitions/s12', '#/definitions/s13', '#/definitions/s12'], + ['#/definitions/s6', '#/definitions/s7', '#/definitions/s6'], + ['#/definitions/s14', '#/definitions/s15', '#/definitions/s14'] + ])) + From aa72608c5d132c8f2ddb88eeedeae8ebddcd35bd Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 11 Dec 2014 16:40:51 +0800 Subject: [PATCH 12/21] refine code remove SwaggerApp.__root_url replace SwaggerApp.__url2obj with SwaggerApp.__url2app remove SwaggerApp._prepare_url, instead, all input url would be refined by utils.normalize_url --- pyswagger/core.py | 119 +++++++++++++++++++-------------------------- pyswagger/utils.py | 20 ++++++++ 2 files changed, 70 insertions(+), 69 deletions(-) diff --git a/pyswagger/core.py b/pyswagger/core.py index 2b59d83..3b2afab 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -29,15 +29,14 @@ class SwaggerApp(object): def __init__(self): """ constructor """ - self.__root_url = None self.__root = None self.__raw = None self.__version = '' self.__op = None self.__m = None - self.__url2obj = {} self.__schemes = [] + self._url2app = {} # TODO: allow init App-wised SCOPE_SEPARATOR @@ -94,38 +93,24 @@ def schemes(self): """ return self.__schemes - @staticmethod - def _prepare_url(url): + def _load_json(self, url, getter=None, parser=None): """ """ - getter = UrlGetter - - p = six.moves.urllib.parse.urlparse(url) - if p.scheme == "": - if p.netloc == "" and p.path != "": - # it should be a file path + if not getter: + getter = UrlGetter + p = six.moves.urllib.parse.urlparse(url) + if p.scheme == 'file' and p.path: getter = LocalGetter(p.path) - url = utils.path2url(url) - else: - raise ValueError('url should be a http-url or file path -- ' + url) - if inspect.isclass(getter): - # default initialization is passing the url - # you can override this behavior by passing an - # initialized getter object. - getter = getter(url) - - return getter, url, p.scheme - - def __load_json(self, url, getter=None, parser=None): - """ - """ - if not getter: - getter, url, _ = self._prepare_url(url) + if inspect.isclass(getter): + # default initialization is passing the url + # you can override this behavior by passing an + # initialized getter object. + getter = getter(url) - if url in self.__url2obj: + if url in self._url2app: # look into cache first - return self.__url2obj[url] + return self._url2app[url].raw # get root document to check its swagger version. obj, _ = six.advance_iterator(getter) @@ -140,23 +125,26 @@ def __load_json(self, url, getter=None, parser=None): with SwaggerContext(tmp, '_tmp_') as ctx: ctx.parse(obj) else: + raise NotImplementedError() with parser(tmp, '_tmp_') as ctx: ctx.parse(obj) - # update map of url to obj - self.__url2obj.update({ - url: tmp['_tmp_'] - }) + self._url2app[url] = self + self.__version = utils.get_swagger_version(tmp['_tmp_']) + self.__raw = tmp['_tmp_'] - return tmp['_tmp_'] - - def __validate(self, url=None): + def _validate(self, url=None): """ check if this Swagger API valid or not. :param bool strict: when in strict mode, exception would be raised if not valid. :return: validation errors :rtype: list of tuple(where, type, msg). """ + if url: + if url not in self._url2app: + raise ValueError('This SwaggerApp is not loaded yet: {0}'.format(url)) + return self._url2app[url]._validate() + v_mod = utils.import_string('.'.join([ 'pyswagger', 'scanner', @@ -172,44 +160,39 @@ def __validate(self, url=None): s = Scanner(self) v = v_mod.Validate() - if url: - s.scan(route=[v], root=self.__url2obj[url]) - else: - s.scan(route=[v], root=self.__raw) - + s.scan(route=[v], root=self.__raw) return v.errs - def __prepare_obj(self, url, strict): + def _prepare_obj(self, url=None, strict=True): """ """ s = Scanner(self) self.validate(url=url, strict=strict) - obj = self.__url2obj[url] + if url: + if url not in self._url2app: + raise ValueError('This SwaggerApp is not loaded yet: {0}'.format(url)) + return self._url2app[url]._prepare_obj(url=None, strict=strict) + if self.version == '1.2': converter = Upgrade() - s.scan(root=obj, route=[converter]) + s.scan(root=self.raw, route=[converter]) obj = converter.swagger # We only have to run this scanner when upgrading from 1.2. # Mainly because we initial BaseObj via NullContext s.scan(root=obj, route=[AssignParent()]) + + self.__root = obj elif self.version == '2.0': - pass + self.__root = self.raw else: raise NotImplementedError('Unsupported Version: {0}'.format(self.__version)) - # back to cache - self.__url2obj[url] = obj - - if url == self.__root_url: - # TODO: ugly... - self.__root = obj - # update schemes if any - if self.__root.schemes and len(self.__root.schemes) > 0: - self.__schemes = self.__root.schemes + if self.__root.schemes and len(self.__root.schemes) > 0: + self.__schemes = self.__root.schemes - s.scan(root=obj, route=[Resolve(), PatchObject()]) + s.scan(root=self.__root, route=[Resolve(), PatchObject()]) @classmethod def load(kls, url, getter=None): @@ -223,15 +206,15 @@ def load(kls, url, getter=None): :raises ValueError: if url is wrong :raises NotImplementedError: the swagger version is not supported. """ - local_getter, url, scheme = kls._prepare_url(url) - getter = getter or local_getter + url = utils.normalize_url(url) app = kls() - obj = app.__load_json(url, getter) - setattr(app, '_' + kls.__name__ + '__root_url', url) - setattr(app, '_' + kls.__name__ + '__version', utils.get_swagger_version(obj)) - setattr(app, '_' + kls.__name__ + '__raw', obj) - app.schemes.append(scheme) + app._load_json(url, getter) + + # update schem if any + p = six.moves.urllib.parse.urlparse(url) + if p.scheme: + app.schemes.append(p.scheme) return app @@ -242,22 +225,20 @@ def validate(self, url=None, strict=True): :return: validation errors :rtype: list of tuple(where, type, msg). """ - errs = self.__validate(url) + errs = self._validate(utils.normalize_url(url)) if strict and len(errs): raise ValueError('this Swagger App contains error: {0}.'.format(len(errs))) return errs - def prepare(self, strict=True): + def prepare(self, url=None, strict=True): """ preparation for loaded json """ - - self.__prepare_obj(url=self.__root_url, strict=strict) - self.__root = self.__url2obj[self.__root_url] - - s = Scanner(self) + url = utils.normalize_url(url) + self._prepare_obj(url=url, strict=strict) # reducer for Operation + s = Scanner(self) tr = TypeReduce() cy = CycleDetector() s.scan(root=self.__root, route=[tr, cy]) @@ -285,7 +266,7 @@ def create(kls, url, strict=True, getter=None): """ app = kls.load(url, getter) - app.prepare() + app.prepare(url) return app diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 85c77e5..c2b1d1e 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -250,6 +250,26 @@ def path2url(p): 'file:', six.moves.urllib.request.pathname2url(p) ) +def normalize_url(url): + """ Normalize url + """ + # TODO: test case + if not url: + return url + + p = six.moves.urllib.parse.urlparse(url) + if p.scheme == "": + if p.netloc == "" and p.path != "": + # it should be a file path + url = path2url(url) + else: + raise ValueError('url should be a http-url or file path -- ' + url) + + return url + +def is_file_url(url): + return url.startswith('file://') + def get_swagger_version(obj): """ get swagger version from loaded json """ From ba330ade98f2343d226b15466d9653d83b5cef98 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 11 Dec 2014 22:27:47 +0800 Subject: [PATCH 13/21] jr_split decompose json-reference into (normalized-url, json-pointer) --- pyswagger/tests/test_utils.py | 21 +++++++++++++++++++++ pyswagger/utils.py | 9 +++++++++ 2 files changed, 30 insertions(+) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 72f3cb5..592a628 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -94,6 +94,27 @@ def test_path2url(self): """ test path2url """ self.assertEqual(utils.path2url('/opt/local/a.json'), 'file:///opt/local/a.json') + def test_jr_split(self): + """ test jr_split """ + self.assertEqual(utils.jr_split( + 'http://test.com/api/swagger.json#/definitions/s1'), ( + 'http://test.com/api/swagger.json', '#/definitions/s1')) + self.assertEqual(utils.jr_split( + 'http://test/com/api/'), ( + 'http://test/com/api/', '')) + self.assertEqual(utils.jr_split( + '#/definitions/s1'), ( + '', '#/definitions/s1')) + self.assertEqual(utils.jr_split( + '/user/tmp/local/ttt'), ( + 'file:///user/tmp/local/ttt', '')) + self.assertEqual(utils.jr_split( + '/user/tmp/local/ttt/'), ( + 'file:///user/tmp/local/ttt/', '')) + self.assertEqual(utils.jr_split( + 'user'), ( + 'file:///user', '')) + class WalkTestCase(unittest.TestCase): """ test for walk """ diff --git a/pyswagger/utils.py b/pyswagger/utils.py index c2b1d1e..3e25f60 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -215,6 +215,15 @@ def _decode(s): return [_decode(ss) for ss in s.split('/')] +def jr_split(s): + """ split a json-reference into (url, json-pointer) + """ + p = six.moves.urllib.parse.urlparse(s) + return ( + normalize_url(six.moves.urllib.parse.urlunparse(p[:5]+('',))), + '#'+p.fragment if p.fragment else '' + ) + def deref(obj): """ dereference $ref """ From d7e84cfef8d18af099acb109d326046a5919795e Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sat, 13 Dec 2014 09:20:56 +0800 Subject: [PATCH 14/21] many refinement - add SwaggerApp.__app_cache to union multiple SwaggerApp with url - add SwaggerApp.__url_load_hook to patch url to load json. - 'getter' parameter in SwaggerApp.create is removed, if needy, call SwaggerApp.load instead. --- pyswagger/core.py | 96 ++++++++++++++++++-------------- pyswagger/tests/v1_2/test_app.py | 2 + 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/pyswagger/core.py b/pyswagger/core.py index 3b2afab..746420e 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -26,7 +26,7 @@ class SwaggerApp(object): sc_path: '#/paths' } - def __init__(self): + def __init__(self, app_cache=None, url_load_hook=None): """ constructor """ self.__root = None @@ -36,7 +36,13 @@ def __init__(self): self.__op = None self.__m = None self.__schemes = [] - self._url2app = {} + + # a map from url to SwaggerApp + self.__app_cache = {} if app_cache == None else app_cache + + # things to make unittest easier, + # all urls to load json would go through this hook + self.__url_load_hook = url_load_hook # TODO: allow init App-wised SCOPE_SEPARATOR @@ -93,12 +99,25 @@ def schemes(self): """ return self.__schemes + @property + def _app_cache(self): + """ internal usage + """ + return self.__app_cache + def _load_json(self, url, getter=None, parser=None): """ """ + if url in self.__app_cache: + # look into cache first + return + if not getter: + # apply hook when use this url to load + local_url = url if not self.__url_load_hook else self.__url_load_hook(url) + getter = UrlGetter - p = six.moves.urllib.parse.urlparse(url) + p = six.moves.urllib.parse.urlparse(local_url) if p.scheme == 'file' and p.path: getter = LocalGetter(p.path) @@ -106,11 +125,7 @@ def _load_json(self, url, getter=None, parser=None): # default initialization is passing the url # you can override this behavior by passing an # initialized getter object. - getter = getter(url) - - if url in self._url2app: - # look into cache first - return self._url2app[url].raw + getter = getter(local_url) # get root document to check its swagger version. obj, _ = six.advance_iterator(getter) @@ -129,21 +144,17 @@ def _load_json(self, url, getter=None, parser=None): with parser(tmp, '_tmp_') as ctx: ctx.parse(obj) - self._url2app[url] = self + self.__app_cache[url] = weakref.proxy(self) # avoid circular reference self.__version = utils.get_swagger_version(tmp['_tmp_']) self.__raw = tmp['_tmp_'] - def _validate(self, url=None): + def _validate(self): """ check if this Swagger API valid or not. :param bool strict: when in strict mode, exception would be raised if not valid. :return: validation errors :rtype: list of tuple(where, type, msg). """ - if url: - if url not in self._url2app: - raise ValueError('This SwaggerApp is not loaded yet: {0}'.format(url)) - return self._url2app[url]._validate() v_mod = utils.import_string('.'.join([ 'pyswagger', @@ -163,16 +174,11 @@ def _validate(self, url=None): s.scan(route=[v], root=self.__raw) return v.errs - def _prepare_obj(self, url=None, strict=True): + def _prepare_obj(self, strict=True): """ """ s = Scanner(self) - self.validate(url=url, strict=strict) - - if url: - if url not in self._url2app: - raise ValueError('This SwaggerApp is not loaded yet: {0}'.format(url)) - return self._url2app[url]._prepare_obj(url=None, strict=strict) + self.validate(strict=strict) if self.version == '1.2': converter = Upgrade() @@ -187,6 +193,7 @@ def _prepare_obj(self, url=None, strict=True): elif self.version == '2.0': self.__root = self.raw else: + # TODO: partial object would go to this place. raise NotImplementedError('Unsupported Version: {0}'.format(self.__version)) if self.__root.schemes and len(self.__root.schemes) > 0: @@ -195,7 +202,7 @@ def _prepare_obj(self, url=None, strict=True): s.scan(root=self.__root, route=[Resolve(), PatchObject()]) @classmethod - def load(kls, url, getter=None): + def load(kls, url, getter=None, app_cache=None, url_load_hook=None): """ load json as a raw SwaggerApp :param str url: url of path of Swagger API definition @@ -206,8 +213,9 @@ def load(kls, url, getter=None): :raises ValueError: if url is wrong :raises NotImplementedError: the swagger version is not supported. """ + url = utils.normalize_url(url) - app = kls() + app = kls(app_cache, url_load_hook) app._load_json(url, getter) @@ -218,24 +226,25 @@ def load(kls, url, getter=None): return app - def validate(self, url=None, strict=True): + def validate(self, strict=True): """ check if this Swagger API valid or not. :param bool strict: when in strict mode, exception would be raised if not valid. :return: validation errors :rtype: list of tuple(where, type, msg). """ - errs = self._validate(utils.normalize_url(url)) + + errs = self._validate() if strict and len(errs): raise ValueError('this Swagger App contains error: {0}.'.format(len(errs))) return errs - def prepare(self, url=None, strict=True): + def prepare(self, strict=True): """ preparation for loaded json """ - url = utils.normalize_url(url) - self._prepare_obj(url=url, strict=strict) + + self._prepare_obj(strict=strict) # reducer for Operation s = Scanner(self) @@ -253,7 +262,7 @@ def prepare(self, url=None, strict=True): raise ValueError('Cycles detected in Schema Object: {0}'.format(cy.cycles['schema'])) @classmethod - def create(kls, url, strict=True, getter=None): + def create(kls, url, strict=True): """ factory of SwaggerApp :param str url: url of path of Swagger API definition @@ -264,9 +273,8 @@ def create(kls, url, strict=True, getter=None): :raises ValueError: if url is wrong :raises NotImplementedError: the swagger version is not supported. """ - - app = kls.load(url, getter) - app.prepare(url) + app = kls.load(url) + app.prepare(strict=strict) return app @@ -275,27 +283,33 @@ def create(kls, url, strict=True, getter=None): """ _create_ = create - def resolve(self, path): + def resolve(self, jref): """ reference resolver - :param str path: a JSON Reference, refer to http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 for details. + :param str jref: a JSON Reference, refer to http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 for details. :return: the referenced object, wrapped by weakref.ProxyType :rtype: weakref.ProxyType :raises ValueError: if path is not valid """ - if path == None or len(path) == 0: + + if jref == None or len(jref) == 0: raise ValueError('Empty Path is not allowed') - if not path.startswith('#'): - raise ValueError('Invalid Path, root element should be \'#\', but [{0}]'.format(path)) + url, jp = utils.jr_split(jref) + if url: + if url not in self.__app_cache: + # This loaded SwaggerApp would be kept in app_cache. + app = SwaggerApp.load(url, app_cache=self.__app_cache, url_load_hook=self.__url_load_hook) + app.prepare() + return self.__app_cache[url].resolve(jp) - if path.endswith('/'): - path = path[:-1] + if not jp.startswith('#'): + raise ValueError('Invalid Path, root element should be \'#\', but [{0}]'.format(jref)) - obj = self.root.resolve(utils.jp_split(path)[1:]) # heading element is #, mapping to self.root + obj = self.root.resolve(utils.jp_split(jp)[1:]) # heading element is #, mapping to self.root if obj == None: - raise ValueError('Unable to resolve path, [{0}]'.format(path)) + raise ValueError('Unable to resolve path, [{0}]'.format(jref)) if isinstance(obj, (six.string_types, int, list, dict)): return obj diff --git a/pyswagger/tests/v1_2/test_app.py b/pyswagger/tests/v1_2/test_app.py index 4e9010b..c34dbe0 100644 --- a/pyswagger/tests/v1_2/test_app.py +++ b/pyswagger/tests/v1_2/test_app.py @@ -132,6 +132,8 @@ def test_ref(self): self.assertEqual(self.app.resolve('#/paths/~1api~1store~1order/post/produces'), ['application/json']) self.assertEqual(self.app.resolve('#/host'), 'petstore.swagger.wordnik.com') + # TODO: resolve with URL part + def test_scope_dict(self): """ ScopeDict is a syntactic suger to access scoped named object, ex. Operation, Model From ba2d4d559c2c888364e527e69fb9a1c327997176 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sat, 13 Dec 2014 09:58:21 +0800 Subject: [PATCH 15/21] refine code to support implicit reference of JSON pointer --- pyswagger/scanner/cycle_detector.py | 16 +++------------- pyswagger/scanner/v2_0/resolve.py | 11 +++-------- pyswagger/utils.py | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/pyswagger/scanner/cycle_detector.py b/pyswagger/scanner/cycle_detector.py index a8984b6..d523624 100644 --- a/pyswagger/scanner/cycle_detector.py +++ b/pyswagger/scanner/cycle_detector.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from ..utils import jp_compose, walk +from ..utils import jp_prefix, walk from ..scan import Dispatcher from ..spec.v2_0.objects import ( Schema, @@ -11,12 +11,7 @@ import six def _out(app, prefix, path): - try: - obj = app.resolve(path) - except: - obj = app.resolve(jp_compose(path, base=prefix)) - # exception would be raised when unable to resolve - + obj = app.resolve(jp_prefix(path, prefix)) r = getattr(obj, '$ref') return [r] if r else [] @@ -42,12 +37,7 @@ def _schema_out_obj(obj, out=None): return out def _schema_out(app, path): - try: - obj = app.resolve(path) - except: - obj = app.resolve(jp_compose(path, base='#/definitions')) - # exception would be raised when unable to resolve - + obj = app.resolve(jp_prefix(path, '#/definitions')) return [] if obj == None else _schema_out_obj(obj) diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index 0306775..4d5fe06 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -9,10 +9,9 @@ Response, PathItem, ) -from ...utils import jp_compose +from ...utils import jp_prefix -# TODO: cyclic detection # TODO: $ref to external docs def is_resolved(obj): @@ -23,11 +22,7 @@ def _resolve(obj, app, prefix): return r = getattr(obj, '$ref') - - try: - ro = app.resolve(r) - except Exception: - ro = app.resolve(jp_compose(r, base=prefix)) + ro = app.resolve(jp_prefix(r, prefix)) if not ro: raise ReferenceError('Unable to resolve: {0}'.format(r)) @@ -80,4 +75,4 @@ def _path_item(self, _, obj, app): # in current object. _merge(obj, app, '#/paths', PathItemContext) - + # TODO: fix merged Operation's url diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 3e25f60..e6a2411 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -227,6 +227,7 @@ def jr_split(s): def deref(obj): """ dereference $ref """ + # TODO: cycle detection cur = obj while cur and getattr(cur, 'ref_obj', None) != None: cur = cur.ref_obj @@ -267,8 +268,8 @@ def normalize_url(url): return url p = six.moves.urllib.parse.urlparse(url) - if p.scheme == "": - if p.netloc == "" and p.path != "": + if p.scheme == '': + if p.netloc == '' and p.path != '': # it should be a file path url = path2url(url) else: @@ -276,6 +277,17 @@ def normalize_url(url): return url +def jp_prefix(jp, prefix): + """ implicit reference of JSON-pointer + """ + # TODO: test case + if jp == None: + return jp + p = six.moves.urllib.parse.urlparse(jp) + if p.scheme == '' and jp.find('#') == -1: + return jp_compose(jp, base=prefix) + return jp + def is_file_url(url): return url.startswith('file://') From 9ce8823e85f90a4ea889ebe4f5a9b70f6e7284c4 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sat, 13 Dec 2014 10:13:33 +0800 Subject: [PATCH 16/21] keep a strong reference of internally resolved SwaggerApp --- pyswagger/core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyswagger/core.py b/pyswagger/core.py index 746420e..28a2542 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -39,6 +39,8 @@ def __init__(self, app_cache=None, url_load_hook=None): # a map from url to SwaggerApp self.__app_cache = {} if app_cache == None else app_cache + # keep a string reference to SwaggerApp when resolve + self.__strong_refs = [] # things to make unittest easier, # all urls to load json would go through this hook @@ -301,6 +303,11 @@ def resolve(self, jref): # This loaded SwaggerApp would be kept in app_cache. app = SwaggerApp.load(url, app_cache=self.__app_cache, url_load_hook=self.__url_load_hook) app.prepare() + + # nothing but only keeping a strong reference of + # loaded SwaggerApp. + self.__strong_refs.append(app) + return self.__app_cache[url].resolve(jp) if not jp.startswith('#'): From a6cf32246f7f0f90c7f5a03e3386ba8786b78d7f Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sat, 13 Dec 2014 21:38:10 +0800 Subject: [PATCH 17/21] complete external document - all $ref are normalized to full JSON referenceall $ref are normalized to full JSON reference - separate scanners: Resolve, PatchObject into 2 runs to make sure the order is expected. --- pyswagger/core.py | 20 +++++- pyswagger/scanner/cycle_detector.py | 6 +- pyswagger/scanner/v2_0/resolve.py | 8 +-- pyswagger/spec/v2_0/parser.py | 2 +- .../tests/data/v2_0/ex/full/swagger.json | 49 +++++++++++++ .../tests/data/v2_0/ex/root/swagger.json | 28 ++++++++ pyswagger/tests/v1_2/test_upgrade.py | 26 +++++-- pyswagger/tests/v2_0/test_circular.py | 54 +++++++++++---- pyswagger/tests/v2_0/test_ex.py | 68 +++++++++++++++++++ pyswagger/utils.py | 40 ++++++++--- 10 files changed, 261 insertions(+), 40 deletions(-) create mode 100644 pyswagger/tests/data/v2_0/ex/full/swagger.json create mode 100644 pyswagger/tests/data/v2_0/ex/root/swagger.json create mode 100644 pyswagger/tests/v2_0/test_ex.py diff --git a/pyswagger/core.py b/pyswagger/core.py index 28a2542..74c7d78 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -26,7 +26,7 @@ class SwaggerApp(object): sc_path: '#/paths' } - def __init__(self, app_cache=None, url_load_hook=None): + def __init__(self, url=None, app_cache=None, url_load_hook=None): """ constructor """ self.__root = None @@ -36,6 +36,7 @@ def __init__(self, app_cache=None, url_load_hook=None): self.__op = None self.__m = None self.__schemes = [] + self.__url=url # a map from url to SwaggerApp self.__app_cache = {} if app_cache == None else app_cache @@ -101,6 +102,12 @@ def schemes(self): """ return self.__schemes + @property + def url(self): + """ + """ + return self.__url + @property def _app_cache(self): """ internal usage @@ -116,6 +123,9 @@ def _load_json(self, url, getter=None, parser=None): if not getter: # apply hook when use this url to load + # note that we didn't cache SwaggerApp with this local_url + + # TODO: test case local_url = url if not self.__url_load_hook else self.__url_load_hook(url) getter = UrlGetter @@ -179,6 +189,9 @@ def _validate(self): def _prepare_obj(self, strict=True): """ """ + if self.__root: + return + s = Scanner(self) self.validate(strict=strict) @@ -201,7 +214,8 @@ def _prepare_obj(self, strict=True): if self.__root.schemes and len(self.__root.schemes) > 0: self.__schemes = self.__root.schemes - s.scan(root=self.__root, route=[Resolve(), PatchObject()]) + s.scan(root=self.__root, route=[Resolve()]) + s.scan(root=self.__root, route=[PatchObject()]) @classmethod def load(kls, url, getter=None, app_cache=None, url_load_hook=None): @@ -217,7 +231,7 @@ def load(kls, url, getter=None, app_cache=None, url_load_hook=None): """ url = utils.normalize_url(url) - app = kls(app_cache, url_load_hook) + app = kls(url, app_cache, url_load_hook) app._load_json(url, getter) diff --git a/pyswagger/scanner/cycle_detector.py b/pyswagger/scanner/cycle_detector.py index d523624..1d0b7bb 100644 --- a/pyswagger/scanner/cycle_detector.py +++ b/pyswagger/scanner/cycle_detector.py @@ -1,5 +1,5 @@ from __future__ import absolute_import -from ..utils import jp_prefix, walk +from ..utils import normalize_jr, walk from ..scan import Dispatcher from ..spec.v2_0.objects import ( Schema, @@ -11,7 +11,7 @@ import six def _out(app, prefix, path): - obj = app.resolve(jp_prefix(path, prefix)) + obj = app.resolve(normalize_jr(path, prefix)) r = getattr(obj, '$ref') return [r] if r else [] @@ -37,7 +37,7 @@ def _schema_out_obj(obj, out=None): return out def _schema_out(app, path): - obj = app.resolve(jp_prefix(path, '#/definitions')) + obj = app.resolve(normalize_jr(path, '#/definitions')) return [] if obj == None else _schema_out_obj(obj) diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index 4d5fe06..6c91f97 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -9,11 +9,9 @@ Response, PathItem, ) -from ...utils import jp_prefix +from ...utils import normalize_jr -# TODO: $ref to external docs - def is_resolved(obj): return getattr(obj, '$ref') == None or obj.ref_obj != None @@ -22,7 +20,7 @@ def _resolve(obj, app, prefix): return r = getattr(obj, '$ref') - ro = app.resolve(jp_prefix(r, prefix)) + ro = app.resolve(normalize_jr(r, prefix)) if not ro: raise ReferenceError('Unable to resolve: {0}'.format(r)) @@ -30,6 +28,7 @@ def _resolve(obj, app, prefix): raise TypeError('Referenced Type mismatch: {0}'.format(r)) obj.update_field('ref_obj', ro) + obj.update_field('$ref', normalize_jr(r, prefix, app.url)) def _merge(obj, app, prefix, ctx): """ resolve $ref as ref_obj, and merge ref_obj to self. @@ -75,4 +74,3 @@ def _path_item(self, _, obj, app): # in current object. _merge(obj, app, '#/paths', PathItemContext) - # TODO: fix merged Operation's url diff --git a/pyswagger/spec/v2_0/parser.py b/pyswagger/spec/v2_0/parser.py index 4865292..405a6d8 100644 --- a/pyswagger/spec/v2_0/parser.py +++ b/pyswagger/spec/v2_0/parser.py @@ -89,7 +89,7 @@ def parse(self, obj=None): # for details ('items', None, SchemaContext), ('properties', ContainerType.dict_, SchemaContext), - # TODO: solution for properties with 2 possible types + # solution for properties with 2 possible types ('additionalProperties', None, AdditionalPropertiesContext), ('allOf', ContainerType.list_, SchemaContext), ]) diff --git a/pyswagger/tests/data/v2_0/ex/full/swagger.json b/pyswagger/tests/data/v2_0/ex/full/swagger.json new file mode 100644 index 0000000..5136323 --- /dev/null +++ b/pyswagger/tests/data/v2_0/ex/full/swagger.json @@ -0,0 +1,49 @@ +{ + "swagger":"2.0", + "host":"test1.com", + "basePath":"/v2", + "produces":[ + "application/json" + ], + "consumes":[ + "application/json" + ], + "schemes":[ + "http", + "https" + ], + "paths":{ + "/user":{ + "get":{ + "responses":{ + "default":{ + "schema":{ + "$ref":"#/definitions/fs1" + } + }, + "404":{ + "schema":{ + "$ref":"#/definitions/fs2" + } + } + } + } + } + }, + "definitions":{ + "fs1":{ + "type":"string" + }, + "fs2":{ + "properties":{ + "id":{ + "type":"integer", + "format":"int32" + }, + "message":{ + "type":"string" + } + } + } + } +} diff --git a/pyswagger/tests/data/v2_0/ex/root/swagger.json b/pyswagger/tests/data/v2_0/ex/root/swagger.json new file mode 100644 index 0000000..dcbdcef --- /dev/null +++ b/pyswagger/tests/data/v2_0/ex/root/swagger.json @@ -0,0 +1,28 @@ +{ + "swagger":"2.0", + "host":"test.com", + "basePath":"/v1", + "produces":[ + "application/json" + ], + "consumes":[ + "application/json" + ], + "schemes":[ + "http", + "https" + ], + "paths":{ + "/full":{ + "$ref":"file:///full/swagger.json#/paths/~1user" + } + }, + "definitions":{ + "s1":{ + "$ref":"file:///full/swagger.json#/definitions/fs1" + }, + "s2":{ + "$ref":"file:///full/swagger.json#/definitions/fs2" + } + } +} diff --git a/pyswagger/tests/v1_2/test_upgrade.py b/pyswagger/tests/v1_2/test_upgrade.py index 5e07119..1d5e6fc 100644 --- a/pyswagger/tests/v1_2/test_upgrade.py +++ b/pyswagger/tests/v1_2/test_upgrade.py @@ -2,6 +2,22 @@ from ..utils import get_test_data_folder import unittest import os +import six + + +folder = get_test_data_folder( + version='1.2', + which='wordnik' +) + +def _pf(s): + return six.moves.urllib.parse.urlunparse(( + 'file', + '', + folder, + '', + '', + s)) class Swagger_Upgrade_TestCase(unittest.TestCase): @@ -9,7 +25,7 @@ class Swagger_Upgrade_TestCase(unittest.TestCase): @classmethod def setUpClass(kls): - kls.app = SwaggerApp._create_(get_test_data_folder(version='1.2', which='wordnik')) + kls.app = SwaggerApp._create_(folder) def test_resource_list(self): """ ResourceList -> Swagger @@ -72,7 +88,7 @@ def test_operation(self): r = o.responses['default'] self.assertEqual(r.headers, {}) self.assertEqual(r.schema.type, 'array') - self.assertEqual(getattr(r.schema.items, '$ref'), '#/definitions/pet!##!Pet') + self.assertEqual(getattr(r.schema.items, '$ref'), _pf('#/definitions/pet!##!Pet')) # createUser o = self.app.root.paths['/api/user'].post @@ -102,7 +118,7 @@ def test_parameter(self): p = [p for p in o.parameters if getattr(p, 'in') == 'body'][0] self.assertEqual(getattr(p, 'in'), 'body') self.assertEqual(p.required, True) - self.assertEqual(getattr(p.schema, '$ref'), '#/definitions/pet!##!Pet') + self.assertEqual(getattr(p.schema, '$ref'), _pf('#/definitions/pet!##!Pet')) # form o = self.app.root.paths['/api/pet/uploadImage'].post @@ -144,7 +160,7 @@ def test_model(self): self.assertEqual(p.maximum, 100) p = d.properties['category'] - self.assertEqual(getattr(p, '$ref'), '#/definitions/pet!##!Category') + self.assertEqual(getattr(p, '$ref'), _pf('#/definitions/pet!##!Category')) p = d.properties['photoUrls'] self.assertEqual(p.type, 'array') @@ -152,7 +168,7 @@ def test_model(self): p = d.properties['tags'] self.assertEqual(p.type, 'array') - self.assertEqual(getattr(p.items, '$ref'), '#/definitions/pet!##!Tag') + self.assertEqual(getattr(p.items, '$ref'), _pf('#/definitions/pet!##!Tag')) p = d.properties['status'] self.assertEqual(p.type, 'string') diff --git a/pyswagger/tests/v2_0/test_circular.py b/pyswagger/tests/v2_0/test_circular.py index 0008880..26fe8fb 100644 --- a/pyswagger/tests/v2_0/test_circular.py +++ b/pyswagger/tests/v2_0/test_circular.py @@ -4,6 +4,7 @@ from ...scan import Scanner import unittest import os +import six class CircularRefTestCase(unittest.TestCase): @@ -19,26 +20,49 @@ def test_path_item_prepare_with_cycle(self): app.prepare() def test_path_item(self): - app = SwaggerApp.create(get_test_data_folder( + folder = get_test_data_folder( version='2.0', which=os.path.join('circular', 'path_item') - )) + ) + + def _pf(s): + return six.moves.urllib.parse.urlunparse(( + 'file', + '', + folder, + '', + '', + s)) + + app = SwaggerApp.create(folder) s = Scanner(app) c = CycleDetector() s.scan(root=app.raw, route=[c]) self.assertEqual(sorted(c.cycles['path_item']), sorted([[ - '#/paths/~1p1', - '#/paths/~1p2', - '#/paths/~1p3', - '#/paths/~1p4', - '#/paths/~1p1' + _pf('#/paths/~1p1'), + _pf('#/paths/~1p2'), + _pf('#/paths/~1p3'), + _pf('#/paths/~1p4'), + _pf('#/paths/~1p1') ]])) def test_schema(self): - app = SwaggerApp.load(get_test_data_folder( + folder = get_test_data_folder( version='2.0', which=os.path.join('circular', 'schema') - )) + ) + + def _pf(s): + return six.moves.urllib.parse.urlunparse(( + 'file', + '', + folder, + '', + '', + s)) + + + app = SwaggerApp.load(folder) app.prepare(strict=False) s = Scanner(app) @@ -46,11 +70,11 @@ def test_schema(self): s.scan(root=app.raw, route=[c]) self.maxDiff = None self.assertEqual(sorted(c.cycles['schema']), sorted([ - ['#/definitions/s10', '#/definitions/s11', '#/definitions/s9', '#/definitions/s10'], - ['#/definitions/s5', '#/definitions/s5'], - ['#/definitions/s1', '#/definitions/s2', '#/definitions/s3', '#/definitions/s4', '#/definitions/s1'], - ['#/definitions/s12', '#/definitions/s13', '#/definitions/s12'], - ['#/definitions/s6', '#/definitions/s7', '#/definitions/s6'], - ['#/definitions/s14', '#/definitions/s15', '#/definitions/s14'] + [_pf('#/definitions/s10'), _pf('#/definitions/s11'), _pf('#/definitions/s9'), _pf('#/definitions/s10')], + [_pf('#/definitions/s5'), _pf('#/definitions/s5')], + [_pf('#/definitions/s1'), _pf('#/definitions/s2'), _pf('#/definitions/s3'), _pf('#/definitions/s4'), _pf('#/definitions/s1')], + [_pf('#/definitions/s12'), _pf('#/definitions/s13'), _pf('#/definitions/s12')], + [_pf('#/definitions/s6'), _pf('#/definitions/s7'), _pf('#/definitions/s6')], + [_pf('#/definitions/s14'), _pf('#/definitions/s15'), _pf('#/definitions/s14')] ])) diff --git a/pyswagger/tests/v2_0/test_ex.py b/pyswagger/tests/v2_0/test_ex.py new file mode 100644 index 0000000..e06eaf5 --- /dev/null +++ b/pyswagger/tests/v2_0/test_ex.py @@ -0,0 +1,68 @@ +from pyswagger import SwaggerApp +from ..utils import get_test_data_folder +import unittest +import os +import six +import weakref + + +folder = get_test_data_folder(version='2.0', which='ex') + + +def _hook(url): + global folder + + p = six.moves.urllib.parse.urlparse(url) + if p.scheme != 'file': + return url + + path = os.path.join(folder, p.path if not p.path.startswith('/') else p.path[1:]) + return six.moves.urllib.parse.urlunparse(p[:2]+(path,)+p[3:]) + + +class ExternalDocumentTestCase(unittest.TestCase): + """ test case for external document """ + + @classmethod + def setUpClass(kls): + global folder + + kls.app = SwaggerApp.load( + url='root', + url_load_hook=_hook + ) + kls.app.prepare() + + def test_resolve(self): + """ make sure resolve with full JSON reference + is the same as resolve with JSON pointer. + """ + p = self.app.resolve('#/paths/~1full') + p_ = self.app.resolve('file:///root#/paths/~1full') + # refer to + # http://stackoverflow.com/questions/10246116/python-dereferencing-weakproxy + # for how to dereferencing weakref + self.assertEqual(p.__repr__(), p_.__repr__()) + + def test_path_item(self): + """ make sure PathItem is correctly merged + """ + p = self.app.resolve('#/paths/~1full') + self.assertNotEqual(p.get, None) + self.assertTrue('default' in p.get.responses) + self.assertTrue('404' in p.get.responses) + + another_p = self.app.resolve('file:///full/swagger.json#/paths/~1user') + self.assertNotEqual(id(p), id(another_p)) + self.assertTrue('default' in another_p.get.responses) + self.assertTrue('404' in another_p.get.responses) + + def test_path_item_url(self): + """ make sure url is correctly patched + """ + p = self.app.resolve('#/paths/~1full') + self.assertEqual(p.get.url, 'test.com/v1/full') + + another_p = self.app.resolve('file:///full/swagger.json#/paths/~1user') + self.assertEqual(another_p.get.url, 'test1.com/v2/user') + diff --git a/pyswagger/utils.py b/pyswagger/utils.py index e6a2411..f96e654 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -277,16 +277,35 @@ def normalize_url(url): return url -def jp_prefix(jp, prefix): - """ implicit reference of JSON-pointer +def normalize_jr(jr, prefix, url=None): + """ normalize JSON reference, also fix + implicit reference of JSON pointer. + input: + - User + - #/definitions/User + - http://test.com/swagger.json#/definitions/User + output: + - http://test.com/swagger.json#/definitions/User """ # TODO: test case - if jp == None: - return jp - p = six.moves.urllib.parse.urlparse(jp) - if p.scheme == '' and jp.find('#') == -1: - return jp_compose(jp, base=prefix) - return jp + if jr == None: + return jr + + p = six.moves.urllib.parse.urlparse(jr) + if p.scheme != '': + return jr + + # it's a JSON reference without url + + # fix implicit reference + jr = jp_compose(jr, base=prefix) if jr.find('#') == -1 else jr + + # prepend url + if url: + p = six.moves.urllib.parse.urlparse(url) + jr = six.moves.urllib.parse.urlunparse(p[:5]+(jr,)) + + return jr def is_file_url(url): return url.startswith('file://') @@ -322,6 +341,11 @@ def walk(start, ofn, cyc=None): if len(ctx[top]): n = ctx[top][0] if n in stk: + # cycles found, + # normalize the representation of cycles, + # start from the smallest vertex, ex. + # 4 -> 5 -> 2 -> 7 -> 9 would produce + # (2, 7, 9, 4, 5) nc = stk[stk.index(n):] ni = nc.index(min(nc)) nc = nc[ni:] + nc[:ni] + [min(nc)] From 98f43191dbf9fd113d4ecb5151c3615cea15315a Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sun, 14 Dec 2014 15:59:17 +0800 Subject: [PATCH 18/21] refine - return ('', '#') when input == '#' for jr_split - refine get_swagger_version for partial swagger json --- pyswagger/tests/test_utils.py | 3 +++ pyswagger/utils.py | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 592a628..19c6d0a 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -114,6 +114,9 @@ def test_jr_split(self): self.assertEqual(utils.jr_split( 'user'), ( 'file:///user', '')) + self.assertEqual(utils.jr_split( + '#'), ( + '', '#')) class WalkTestCase(unittest.TestCase): diff --git a/pyswagger/utils.py b/pyswagger/utils.py index f96e654..8c02ebd 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -218,6 +218,9 @@ def _decode(s): def jr_split(s): """ split a json-reference into (url, json-pointer) """ + if s == '#': + return ('', '#') + p = six.moves.urllib.parse.urlparse(s) return ( normalize_url(six.moves.urllib.parse.urlunparse(p[:5]+('',))), @@ -315,7 +318,11 @@ def get_swagger_version(obj): # TODO: test case if isinstance(obj, dict): - return obj['swaggerVersion'] if 'swaggerVersion' in obj else obj['swagger'] + if 'swaggerVersion' in obj: + return obj['swaggerVersion'] + elif 'swagger' in obj: + return obj['swagger'] + return None else: # should be an instance of BaseObj return obj.swaggerVersion if hasattr(obj, 'swaggerVersion') else obj.swagger From 1e884d1c32b24e16d0af53b9443a9da6647f6fd1 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sun, 14 Dec 2014 16:17:22 +0800 Subject: [PATCH 19/21] add __swagger_version__ in pyswagger.base.BaseObj to recognize version of swagger for partial swagger.json --- pyswagger/base.py | 4 ++++ pyswagger/spec/v1_2/objects.py | 38 +++++++++++++++++++--------------- pyswagger/spec/v2_0/objects.py | 23 +++++++++++++------- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/pyswagger/base.py b/pyswagger/base.py index 38f1982..b0ff242 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -180,6 +180,10 @@ class BaseObj(object): # - tuple(string, default-value): a field name with default value __swagger_fields__ = [] + + # Swagger Version this object belonging to + __swagger_version__ = None + def __init__(self, ctx): """ constructor diff --git a/pyswagger/spec/v1_2/objects.py b/pyswagger/spec/v1_2/objects.py index 9b9c203..e86df08 100644 --- a/pyswagger/spec/v1_2/objects.py +++ b/pyswagger/spec/v1_2/objects.py @@ -4,7 +4,11 @@ import copy -class Items(six.with_metaclass(FieldMeta, BaseObj)): +class BaseObj_v1_2(BaseObj): + __swagger_version__ = '1.2' + + +class Items(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Items Object """ __swagger_fields__ = [ @@ -20,7 +24,7 @@ class ItemsContext(Context): __swagger_ref_object__ = Items -class DataTypeObj(BaseObj): +class DataTypeObj(BaseObj_v1_2): """ Data Type Fields """ __swagger_fields__ = [ @@ -52,7 +56,7 @@ def __init__(self, ctx): super(DataTypeObj, self).__init__(ctx) -class Scope(six.with_metaclass(FieldMeta, BaseObj)): +class Scope(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Scope Object """ @@ -61,7 +65,7 @@ class Scope(six.with_metaclass(FieldMeta, BaseObj)): ] -class LoginEndpoint(six.with_metaclass(FieldMeta, BaseObj)): +class LoginEndpoint(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ LoginEndpoint Object """ @@ -70,7 +74,7 @@ class LoginEndpoint(six.with_metaclass(FieldMeta, BaseObj)): ] -class Implicit(six.with_metaclass(FieldMeta, BaseObj)): +class Implicit(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Implicit Object """ @@ -80,7 +84,7 @@ class Implicit(six.with_metaclass(FieldMeta, BaseObj)): ] -class TokenRequestEndpoint(six.with_metaclass(FieldMeta, BaseObj)): +class TokenRequestEndpoint(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ TokenRequestEndpoint Object """ @@ -91,7 +95,7 @@ class TokenRequestEndpoint(six.with_metaclass(FieldMeta, BaseObj)): ] -class TokenEndpoint(six.with_metaclass(FieldMeta, BaseObj)): +class TokenEndpoint(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ TokenEndpoint Object """ @@ -101,7 +105,7 @@ class TokenEndpoint(six.with_metaclass(FieldMeta, BaseObj)): ] -class AuthorizationCode(six.with_metaclass(FieldMeta, BaseObj)): +class AuthorizationCode(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ AuthorizationCode Object """ @@ -111,7 +115,7 @@ class AuthorizationCode(six.with_metaclass(FieldMeta, BaseObj)): ] -class GrantType(six.with_metaclass(FieldMeta, BaseObj)): +class GrantType(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ GrantType Object """ @@ -121,7 +125,7 @@ class GrantType(six.with_metaclass(FieldMeta, BaseObj)): ] -class Authorizations(six.with_metaclass(FieldMeta, BaseObj)): +class Authorizations(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Authorizations Object """ @@ -130,7 +134,7 @@ class Authorizations(six.with_metaclass(FieldMeta, BaseObj)): ] -class Authorization(six.with_metaclass(FieldMeta, BaseObj)): +class Authorization(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Authorization Object """ @@ -146,7 +150,7 @@ def get_name(self, path): return path.split('/', 3)[2] -class ResponseMessage(six.with_metaclass(FieldMeta, BaseObj)): +class ResponseMessage(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ ResponseMessage Object """ @@ -191,7 +195,7 @@ def get_name(self, path): return self.nickname -class Api(six.with_metaclass(FieldMeta, BaseObj)): +class Api(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Api Object """ @@ -208,7 +212,7 @@ class Property(six.with_metaclass(FieldMeta, DataTypeObj)): __swagger_fields__ = [] -class Model(six.with_metaclass(FieldMeta, BaseObj)): +class Model(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Model Object """ @@ -227,7 +231,7 @@ def get_name(self, path): return self.id -class Resource(six.with_metaclass(FieldMeta, BaseObj)): +class Resource(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Resource Object """ @@ -270,7 +274,7 @@ def get_name(self, path): return path.split('/', 3)[2] -class Info(six.with_metaclass(FieldMeta, BaseObj)): +class Info(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Info Object """ @@ -284,7 +288,7 @@ class Info(six.with_metaclass(FieldMeta, BaseObj)): ] -class ResourceList(six.with_metaclass(FieldMeta, BaseObj)): +class ResourceList(six.with_metaclass(FieldMeta, BaseObj_v1_2)): """ Resource List Object """ __swagger_fields__ = [ diff --git a/pyswagger/spec/v2_0/objects.py b/pyswagger/spec/v2_0/objects.py index 43011b5..b9e1b01 100644 --- a/pyswagger/spec/v2_0/objects.py +++ b/pyswagger/spec/v2_0/objects.py @@ -6,7 +6,11 @@ import copy -class BaseSchema(BaseObj): +class BaseObj_v2_0(BaseObj): + __swagger_version__ = '2.0' + + +class BaseSchema(BaseObj_v2_0): """ Base type for Items, Schema, Parameter, Header """ @@ -64,6 +68,7 @@ class Schema(six.with_metaclass(FieldMeta, BaseSchema)): ('additionalProperties', True), ('discriminator', None), + # TODO: readonly not handled ('readOnly', None), # pyswagger only @@ -75,7 +80,7 @@ def _prim_(self, v): return primitives.prim_factory(self, v) -class Swagger(six.with_metaclass(FieldMeta, BaseObj)): +class Swagger(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Swagger Object """ @@ -97,7 +102,7 @@ class Swagger(six.with_metaclass(FieldMeta, BaseObj)): ] -class Info(six.with_metaclass(FieldMeta, BaseObj)): +class Info(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Info Object """ @@ -113,6 +118,7 @@ class Parameter(six.with_metaclass(FieldMeta, BaseSchema)): __swagger_fields__ = [ # Reference Object ('$ref', None), + # TODO: test case ('name', None), ('in', None), @@ -142,13 +148,14 @@ class Header(six.with_metaclass(FieldMeta, BaseSchema)): ] -class Response(six.with_metaclass(FieldMeta, BaseObj)): +class Response(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Response Object """ __swagger_fields__ = [ # Reference Object ('$ref', None), + # TODO: test case ('schema', None), ('headers', {}), @@ -158,7 +165,7 @@ class Response(six.with_metaclass(FieldMeta, BaseObj)): ] -class Operation(six.with_metaclass(FieldMeta, BaseObj)): +class Operation(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Operation Object """ @@ -208,7 +215,7 @@ def _convert_parameter(p): io.SwaggerRequest(op=self, params=params), io.SwaggerResponse(self) -class PathItem(six.with_metaclass(FieldMeta, BaseObj)): +class PathItem(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Path Item Object """ @@ -230,7 +237,7 @@ class PathItem(six.with_metaclass(FieldMeta, BaseObj)): ] -class SecurityScheme(six.with_metaclass(FieldMeta, BaseObj)): +class SecurityScheme(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Security Scheme Object """ @@ -245,7 +252,7 @@ class SecurityScheme(six.with_metaclass(FieldMeta, BaseObj)): ] -class Tag(six.with_metaclass(FieldMeta, BaseObj)): +class Tag(six.with_metaclass(FieldMeta, BaseObj_v2_0)): """ Tag Object """ From 1ab2392ea487e3e747cfbb8ed08da7e8993e0d9c Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sun, 14 Dec 2014 16:17:50 +0800 Subject: [PATCH 20/21] append '#' for JSON reference without JSON pointer --- pyswagger/tests/test_utils.py | 8 ++++---- pyswagger/utils.py | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 19c6d0a..0c96a36 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -101,19 +101,19 @@ def test_jr_split(self): 'http://test.com/api/swagger.json', '#/definitions/s1')) self.assertEqual(utils.jr_split( 'http://test/com/api/'), ( - 'http://test/com/api/', '')) + 'http://test/com/api/', '#')) self.assertEqual(utils.jr_split( '#/definitions/s1'), ( '', '#/definitions/s1')) self.assertEqual(utils.jr_split( '/user/tmp/local/ttt'), ( - 'file:///user/tmp/local/ttt', '')) + 'file:///user/tmp/local/ttt', '#')) self.assertEqual(utils.jr_split( '/user/tmp/local/ttt/'), ( - 'file:///user/tmp/local/ttt/', '')) + 'file:///user/tmp/local/ttt/', '#')) self.assertEqual(utils.jr_split( 'user'), ( - 'file:///user', '')) + 'file:///user', '#')) self.assertEqual(utils.jr_split( '#'), ( '', '#')) diff --git a/pyswagger/utils.py b/pyswagger/utils.py index 8c02ebd..b2739f7 100644 --- a/pyswagger/utils.py +++ b/pyswagger/utils.py @@ -218,13 +218,10 @@ def _decode(s): def jr_split(s): """ split a json-reference into (url, json-pointer) """ - if s == '#': - return ('', '#') - p = six.moves.urllib.parse.urlparse(s) return ( normalize_url(six.moves.urllib.parse.urlunparse(p[:5]+('',))), - '#'+p.fragment if p.fragment else '' + '#'+p.fragment if p.fragment else '#' ) def deref(obj): From 802fd215eab29bac790c72c4c827cd596f1a7c96 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sun, 14 Dec 2014 17:26:01 +0800 Subject: [PATCH 21/21] external document with partial swagger.json swagger.json with PathItem and Schema is allowed. --- pyswagger/core.py | 45 +++++++++++-------- pyswagger/scanner/v2_0/patch_obj.py | 28 +++++++----- pyswagger/scanner/v2_0/resolve.py | 15 ++++--- .../v2_0/ex/partial/path_item/swagger.json | 12 +++++ .../data/v2_0/ex/partial/schema/swagger.json | 6 +++ .../tests/data/v2_0/ex/root/swagger.json | 14 +++++- pyswagger/tests/test_utils.py | 3 ++ pyswagger/tests/v1_2/test_app.py | 1 - pyswagger/tests/v2_0/test_ex.py | 38 +++++++++++++--- 9 files changed, 117 insertions(+), 45 deletions(-) create mode 100644 pyswagger/tests/data/v2_0/ex/partial/path_item/swagger.json create mode 100644 pyswagger/tests/data/v2_0/ex/partial/schema/swagger.json diff --git a/pyswagger/core.py b/pyswagger/core.py index 74c7d78..28c889e 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -142,22 +142,25 @@ def _load_json(self, url, getter=None, parser=None): # get root document to check its swagger version. obj, _ = six.advance_iterator(getter) tmp = {'_tmp_': {}} - if not parser: - if utils.get_swagger_version(obj) == '1.2': - # swagger 1.2 - with ResourceListContext(tmp, '_tmp_') as ctx: - ctx.parse(getter, obj) - else: - # swagger 2.0 - with SwaggerContext(tmp, '_tmp_') as ctx: - ctx.parse(obj) - else: - raise NotImplementedError() + version = utils.get_swagger_version(obj) + if version == '1.2': + # swagger 1.2 + with ResourceListContext(tmp, '_tmp_') as ctx: + ctx.parse(getter, obj) + elif version == '2.0': + # swagger 2.0 + with SwaggerContext(tmp, '_tmp_') as ctx: + ctx.parse(obj) + elif version == None and parser: with parser(tmp, '_tmp_') as ctx: ctx.parse(obj) + version = tmp['_tmp_'].__swagger_version__ if hasattr(tmp['_tmp_'], '__swagger_version__') else version + else: + raise NotImplementedError('Unsupported Swagger Version: {0} from {1}'.format(version, url)) + self.__app_cache[url] = weakref.proxy(self) # avoid circular reference - self.__version = utils.get_swagger_version(tmp['_tmp_']) + self.__version = version self.__raw = tmp['_tmp_'] def _validate(self): @@ -211,14 +214,15 @@ def _prepare_obj(self, strict=True): # TODO: partial object would go to this place. raise NotImplementedError('Unsupported Version: {0}'.format(self.__version)) - if self.__root.schemes and len(self.__root.schemes) > 0: - self.__schemes = self.__root.schemes + if hasattr(self.__root, 'schemes') and self.__root.schemes: + if len(self.__root.schemes) > 0: + self.__schemes = self.__root.schemes s.scan(root=self.__root, route=[Resolve()]) s.scan(root=self.__root, route=[PatchObject()]) @classmethod - def load(kls, url, getter=None, app_cache=None, url_load_hook=None): + def load(kls, url, getter=None, parser=None, app_cache=None, url_load_hook=None): """ load json as a raw SwaggerApp :param str url: url of path of Swagger API definition @@ -233,7 +237,7 @@ def load(kls, url, getter=None, app_cache=None, url_load_hook=None): url = utils.normalize_url(url) app = kls(url, app_cache, url_load_hook) - app._load_json(url, getter) + app._load_json(url, getter, parser) # update schem if any p = six.moves.urllib.parse.urlparse(url) @@ -271,7 +275,10 @@ def prepare(self, strict=True): # 'op' -- shortcut for Operation with tag and operaionId self.__op = utils.ScopeDict(tr.op) # 'm' -- shortcut for model in Swagger 1.2 - self.__m = utils.ScopeDict(self.__root.definitions) + if hasattr(self.__root, 'definitions'): + self.__m = utils.ScopeDict(self.__root.definitions) + else: + self.__m = utils.ScopeDict({}) # cycle detection if len(cy.cycles['schema']) > 0 and strict: @@ -299,7 +306,7 @@ def create(kls, url, strict=True): """ _create_ = create - def resolve(self, jref): + def resolve(self, jref, parser=None): """ reference resolver :param str jref: a JSON Reference, refer to http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03 for details. @@ -315,7 +322,7 @@ def resolve(self, jref): if url: if url not in self.__app_cache: # This loaded SwaggerApp would be kept in app_cache. - app = SwaggerApp.load(url, app_cache=self.__app_cache, url_load_hook=self.__url_load_hook) + app = SwaggerApp.load(url, parser=parser, app_cache=self.__app_cache, url_load_hook=self.__url_load_hook) app.prepare() # nothing but only keeping a strong reference of diff --git a/pyswagger/scanner/v2_0/patch_obj.py b/pyswagger/scanner/v2_0/patch_obj.py index 384dd7d..a052655 100644 --- a/pyswagger/scanner/v2_0/patch_obj.py +++ b/pyswagger/scanner/v2_0/patch_obj.py @@ -1,6 +1,6 @@ from __future__ import absolute_import from ...scan import Dispatcher -from ...spec.v2_0.objects import PathItem, Operation, Schema +from ...spec.v2_0.objects import PathItem, Operation, Schema, Swagger from ...spec.v2_0.parser import PathItemContext from ...utils import jp_split, scope_split @@ -18,17 +18,19 @@ class Disp(Dispatcher): pass def _operation(self, path, obj, app): """ """ - # produces/consumes - obj.update_field('produces', app.root.produces if len(obj.produces) == 0 else obj.produces) - obj.update_field('consumes', app.root.consumes if len(obj.consumes) == 0 else obj.consumes) + if isinstance(app.root, Swagger): + # produces/consumes + obj.update_field('produces', app.root.produces if len(obj.produces) == 0 else obj.produces) + obj.update_field('consumes', app.root.consumes if len(obj.consumes) == 0 else obj.consumes) # combine parameters from PathItem - for p in obj._parent_.parameters: - for pp in obj.parameters: - if p.name == pp.name: - break - else: - obj.parameters.append(p) + if obj._parent_: + for p in obj._parent_.parameters: + for pp in obj.parameters: + if p.name == pp.name: + break + else: + obj.parameters.append(p) # schemes obj.update_field('schemes', app.schemes if len(obj.schemes) == 0 else obj.schemes) @@ -37,7 +39,11 @@ def _operation(self, path, obj, app): def _path_item(self, path, obj, app): """ """ - url = app.root.host + (app.root.basePath or '') + jp_split(path)[-1] + if isinstance(app.root, Swagger): + url = app.root.host + (app.root.basePath or '') + jp_split(path)[-1] + else: + url = None + for c in PathItemContext.__swagger_child__: o = getattr(obj, c[0]) if isinstance(o, Operation): diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index 6c91f97..140ab79 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -1,6 +1,9 @@ from __future__ import absolute_import from ...scan import Dispatcher from ...spec.v2_0.parser import ( + SchemaContext, + ParameterContext, + ResponseContext, PathItemContext ) from ...spec.v2_0.objects import ( @@ -15,12 +18,12 @@ def is_resolved(obj): return getattr(obj, '$ref') == None or obj.ref_obj != None -def _resolve(obj, app, prefix): +def _resolve(obj, app, prefix, parser): if is_resolved(obj): return r = getattr(obj, '$ref') - ro = app.resolve(normalize_jr(r, prefix)) + ro = app.resolve(normalize_jr(r, prefix), parser) if not ro: raise ReferenceError('Unable to resolve: {0}'.format(r)) @@ -38,7 +41,7 @@ def _merge(obj, app, prefix, ctx): cur = obj to_resolve = [] while not is_resolved(cur): - _resolve(cur, app, prefix) + _resolve(cur, app, prefix, ctx) to_resolve.append(cur) cur = cur.ref_obj if cur.ref_obj else cur @@ -56,15 +59,15 @@ class Disp(Dispatcher): pass @Disp.register([Schema]) def _schema(self, _, obj, app): - _resolve(obj, app, '#/definitions') + _resolve(obj, app, '#/definitions', SchemaContext) @Disp.register([Parameter]) def _parameter(self, _, obj, app): - _resolve(obj, app, '#/parameters') + _resolve(obj, app, '#/parameters', ParameterContext) @Disp.register([Response]) def _response(self, _, obj, app): - _resolve(obj, app, '#/responses') + _resolve(obj, app, '#/responses', ResponseContext) @Disp.register([PathItem]) def _path_item(self, _, obj, app): diff --git a/pyswagger/tests/data/v2_0/ex/partial/path_item/swagger.json b/pyswagger/tests/data/v2_0/ex/partial/path_item/swagger.json new file mode 100644 index 0000000..b98a5a0 --- /dev/null +++ b/pyswagger/tests/data/v2_0/ex/partial/path_item/swagger.json @@ -0,0 +1,12 @@ +{ + "get":{ + "response":{ + "description":"path_item, get, response" + } + }, + "put":{ + "response":{ + "description":"path_item, put, response" + } + } +} \ No newline at end of file diff --git a/pyswagger/tests/data/v2_0/ex/partial/schema/swagger.json b/pyswagger/tests/data/v2_0/ex/partial/schema/swagger.json new file mode 100644 index 0000000..60ce799 --- /dev/null +++ b/pyswagger/tests/data/v2_0/ex/partial/schema/swagger.json @@ -0,0 +1,6 @@ +{ + "type":"array", + "items":{ + "$ref":"file:///root/swagger.json#/definitions/s3" + } +} \ No newline at end of file diff --git a/pyswagger/tests/data/v2_0/ex/root/swagger.json b/pyswagger/tests/data/v2_0/ex/root/swagger.json index dcbdcef..ab3f992 100644 --- a/pyswagger/tests/data/v2_0/ex/root/swagger.json +++ b/pyswagger/tests/data/v2_0/ex/root/swagger.json @@ -15,6 +15,9 @@ "paths":{ "/full":{ "$ref":"file:///full/swagger.json#/paths/~1user" + }, + "/partial":{ + "$ref":"file:///partial/path_item/swagger.json" } }, "definitions":{ @@ -23,6 +26,15 @@ }, "s2":{ "$ref":"file:///full/swagger.json#/definitions/fs2" + }, + "s3":{ + "type":"string" + }, + "s4":{ + "type":"array", + "items":{ + "$ref":"file:///partial/schema/swagger.json" + } } } -} +} \ No newline at end of file diff --git a/pyswagger/tests/test_utils.py b/pyswagger/tests/test_utils.py index 0c96a36..bb6f921 100644 --- a/pyswagger/tests/test_utils.py +++ b/pyswagger/tests/test_utils.py @@ -117,6 +117,9 @@ def test_jr_split(self): self.assertEqual(utils.jr_split( '#'), ( '', '#')) + self.assertEqual(utils.jr_split( + '//'), ( + '', '#')) class WalkTestCase(unittest.TestCase): diff --git a/pyswagger/tests/v1_2/test_app.py b/pyswagger/tests/v1_2/test_app.py index c34dbe0..96be819 100644 --- a/pyswagger/tests/v1_2/test_app.py +++ b/pyswagger/tests/v1_2/test_app.py @@ -125,7 +125,6 @@ def test_ref(self): """ test ref function """ self.assertRaises(ValueError, self.app.resolve, None) self.assertRaises(ValueError, self.app.resolve, '') - self.assertRaises(ValueError, self.app.resolve, '//') self.assertTrue(isinstance(self.app.resolve('#/definitions/user!##!User'), Schema)) self.assertTrue(isinstance(self.app.resolve('#/paths/~1api~1user~1{username}/put'), Operation)) diff --git a/pyswagger/tests/v2_0/test_ex.py b/pyswagger/tests/v2_0/test_ex.py index e06eaf5..e4409bd 100644 --- a/pyswagger/tests/v2_0/test_ex.py +++ b/pyswagger/tests/v2_0/test_ex.py @@ -3,7 +3,6 @@ import unittest import os import six -import weakref folder = get_test_data_folder(version='2.0', which='ex') @@ -28,7 +27,7 @@ def setUpClass(kls): global folder kls.app = SwaggerApp.load( - url='root', + url='file:///root/swagger.json', url_load_hook=_hook ) kls.app.prepare() @@ -38,13 +37,13 @@ def test_resolve(self): is the same as resolve with JSON pointer. """ p = self.app.resolve('#/paths/~1full') - p_ = self.app.resolve('file:///root#/paths/~1full') + p_ = self.app.resolve('file:///root/swagger.json#/paths/~1full') # refer to # http://stackoverflow.com/questions/10246116/python-dereferencing-weakproxy # for how to dereferencing weakref self.assertEqual(p.__repr__(), p_.__repr__()) - def test_path_item(self): + def test_full_path_item(self): """ make sure PathItem is correctly merged """ p = self.app.resolve('#/paths/~1full') @@ -57,12 +56,37 @@ def test_path_item(self): self.assertTrue('default' in another_p.get.responses) self.assertTrue('404' in another_p.get.responses) - def test_path_item_url(self): + def test_full_path_item_url(self): """ make sure url is correctly patched """ p = self.app.resolve('#/paths/~1full') self.assertEqual(p.get.url, 'test.com/v1/full') - another_p = self.app.resolve('file:///full/swagger.json#/paths/~1user') - self.assertEqual(another_p.get.url, 'test1.com/v2/user') + original_p = self.app.resolve('file:///full/swagger.json#/paths/~1user') + self.assertEqual(original_p.get.url, 'test1.com/v2/user') + + def test_partial_path_item(self): + """ make sure partial swagger.json with PathItem + loaded correctly. + """ + p = self.app.resolve('#/paths/~1partial') + self.assertEqual(p.get.url, 'test.com/v1/partial') + + original_p = self.app.resolve('file:///partial/path_item/swagger.json') + self.assertEqual(original_p.get.url, None) + + def test_partial_schema(self): + """ make sure partial swagger.json with Schema + loaded correctly. + """ + p = self.app.resolve('#/definitions/s4') + original_p = self.app.resolve('file:///partial/schema/swagger.json') + + # refer to + # http://stackoverflow.com/questions/10246116/python-dereferencing-weakproxy + # for how to dereferencing weakref + self.assertEqual(p.items.ref_obj.__repr__(), original_p.__repr__()) + + p_ = self.app.resolve('#/definitions/s3') + self.assertEqual(p_.__repr__(), original_p.items.ref_obj.__repr__())