From e3eec92f6d3c808870e8e9b42a566d177399b84f Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Wed, 26 Nov 2014 12:39:33 +0800 Subject: [PATCH 1/7] should resolve to different path --- pyswagger/scanner/v2_0/resolve.py | 52 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index 9a5e585..08a6367 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -6,7 +6,27 @@ Response, PathItem, ) -from ...utils import jp_split, jp_compose +from ...utils import jp_compose + + +# TODO: test case + +def _resolve(obj, app, prefix): + r = getattr(obj, '$ref') + if r == None: + return + + try: + ro = app.resolve(r) + except Exception: + ro = app.resolve(jp_compose(r, base=prefix)) + + if not ro: + raise ReferenceError('Unable to resolve: {0}'.format(r)) + if ro.__class__ != obj.__class__: + raise TypeError('Referenced Type mismatch: {0}'.format(r)) + + obj.update_field('ref_obj', ro) class Resolve(object): @@ -14,23 +34,17 @@ class Resolve(object): class Disp(Dispatcher): pass + @Disp.register([Schema, Parameter, Response, PathItem]) - def _resolve(self, path, obj, app): - r = getattr(obj, '$ref') - if r == None: - return - - try: - ro = app.resolve(r) - except Exception: - ps = jp_split(path)[:2] - ps.append(r) - ro = app.resolve(jp_compose(ps)) - - if not ro: - raise ReferenceError('Unable to resolve: {0}'.format(r)) - if ro.__class__ != obj.__class__: - raise TypeError('Referenced Type mismatch: {0}'.format(r)) - - obj.update_field('ref_obj', ro) + def _schema(self, _, obj, app): + _resolve(obj, app, '#/definitions') + + def _parameter(self, _, obj, app): + _resolve(obj, app, '#/parameters') + + def _response(self, _, obj, app): + _resolve(obj, app, '#/responses') + + def _path_item(self, _, obj, app): + _resolve(obj, app, '#/paths') From e8a1619fb657d1c000b04bb2d93048a4497b5e8a Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Wed, 26 Nov 2014 12:40:04 +0800 Subject: [PATCH 2/7] accept single string token --- pyswagger/base.py | 4 ++++ pyswagger/core.py | 1 + 2 files changed, 5 insertions(+) diff --git a/pyswagger/base.py b/pyswagger/base.py index abba025..f162904 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -217,6 +217,10 @@ def update_field(self, f, obj): def resolve(self, ts): """ resolve a list of tokens to an child object """ + # TODO: test case + if isinstance(ts, six.string_types): + ts = [ts] + obj = self while len(ts) > 0: t = ts.pop(0) diff --git a/pyswagger/core.py b/pyswagger/core.py index 2a37a0f..19f6835 100644 --- a/pyswagger/core.py +++ b/pyswagger/core.py @@ -244,6 +244,7 @@ def resolve(self, path): :rtype: weakref.ProxyType :raises ValueError: if path is not valid """ + # TODO: test case if path == None or len(path) == 0: raise ValueError('Empty Path is not allowed') From 05e1f9981ee9adb0149d2c1c46f06c84f46a07fe Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Wed, 26 Nov 2014 12:40:15 +0800 Subject: [PATCH 3/7] test case for operation access --- pyswagger/tests/v2_0/test_op_access.py | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 pyswagger/tests/v2_0/test_op_access.py diff --git a/pyswagger/tests/v2_0/test_op_access.py b/pyswagger/tests/v2_0/test_op_access.py new file mode 100644 index 0000000..4e9e529 --- /dev/null +++ b/pyswagger/tests/v2_0/test_op_access.py @@ -0,0 +1,33 @@ +from pyswagger import SwaggerApp, utils +from ..utils import get_test_data_folder +import unittest + +def _check(u, op): + u.assertEqual(op.operationId, 'addPet') + +class OperationAccessTestCase(unittest.TestCase): + """ test for methods to access Operation """ + + @classmethod + def setUpClass(kls): + kls.app = SwaggerApp._create_(get_test_data_folder(version='2.0', which='wordnik')) + + def test_resolve(self): + """ + """ + _check(self, self.app.resolve(utils.jp_compose(['#', 'paths', '/pet', 'post']))) + + def test_cascade_resolve(self): + """ + """ + path = self.app.resolve(utils.jp_compose(['#', 'paths', '/pet'])) + + _check(self, path.resolve('post')) + _check(self, path.post) + + def test_tag_operationId(self): + """ + """ + _check(self, self.app.op['pet', 'addPet']) + _check(self, self.app.op['addPet']) + From f9d1e418205e16f727bbbc0331e5358bb918bcda Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Thu, 27 Nov 2014 14:46:02 +0800 Subject: [PATCH 4/7] add merge to BaseObj --- pyswagger/base.py | 8 ++++++++ pyswagger/tests/test_base.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/pyswagger/base.py b/pyswagger/base.py index f162904..5e1e843 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -234,6 +234,14 @@ def resolve(self, ts): return obj + def merge(self, other): + """ merge properties from other object + """ + for name, _ in self.__swagger_fields__: + v = getattr(other, name) + if v and getattr(self, name) == None: + self.update_field(name, v) + @property def _parent_(self): """ get parent object diff --git a/pyswagger/tests/test_base.py b/pyswagger/tests/test_base.py index 33e93db..4eab73f 100644 --- a/pyswagger/tests/test_base.py +++ b/pyswagger/tests/test_base.py @@ -83,3 +83,24 @@ def test_field_default_value(self): o2 = TestObj(base.NullContext()) self.assertTrue(id(o1.a) != id(o2.a)) + def test_merge(self): + """ test merge function """ + tmp = {'t': {}} + obj1 = {'a': [{}, {}, {}], 'd': {}} + obj2 = {'a': [{}]} + + with TestContext(tmp, 't') as ctx: + ctx.parse(obj1) + o1 = tmp['t'] + + with TestContext(tmp, 't') as ctx: + ctx.parse(obj2) + o2 = tmp['t'] + + self.assertTrue(len(o2.a), 1) + self.assertEqual(o2.d, None) + + o2.merge(o1) + self.assertTrue(len(o2.a), 1) + self.assertTrue(isinstance(o2.d, ChildObj)) + From 499f8e262c4403ca8f326b90d77158688d611427 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Fri, 28 Nov 2014 18:54:46 +0800 Subject: [PATCH 5/7] missing case for weakref.ProxyTypes we can't call weakref.proxy on weakref.ProxyTypes --- pyswagger/base.py | 8 +++++++- pyswagger/tests/test_base.py | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyswagger/base.py b/pyswagger/base.py index 5e1e843..791d44d 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -240,7 +240,13 @@ def merge(self, other): for name, _ in self.__swagger_fields__: v = getattr(other, name) if v and getattr(self, name) == None: - self.update_field(name, v) + if isinstance(v, weakref.ProxyTypes): + # TODO: test case + self.update_field(name, v) + elif isinstance(v, BaseObj): + self.update_field(name, weakref.proxy(v)) + else: + self.update_field(name, v) @property def _parent_(self): diff --git a/pyswagger/tests/test_base.py b/pyswagger/tests/test_base.py index 4eab73f..db301d8 100644 --- a/pyswagger/tests/test_base.py +++ b/pyswagger/tests/test_base.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from pyswagger import base import unittest +import weakref import six @@ -16,6 +17,7 @@ class TestObj(six.with_metaclass(base.FieldMeta, base.BaseObj)): ('b', {}), ('c', {}), ('d', None), + ('f', None) ] class TestContext(base.Context): @@ -86,7 +88,7 @@ def test_field_default_value(self): def test_merge(self): """ test merge function """ tmp = {'t': {}} - obj1 = {'a': [{}, {}, {}], 'd': {}} + obj1 = {'a': [{}, {}, {}], 'd': {}, 'f': ''} obj2 = {'a': [{}]} with TestContext(tmp, 't') as ctx: @@ -102,5 +104,7 @@ def test_merge(self): o2.merge(o1) self.assertTrue(len(o2.a), 1) + self.assertEqaul(o2.f, '') self.assertTrue(isinstance(o2.d, ChildObj)) + self.assertTrue(isinstance(o2.d, weakref.ProxyTypes)) From b9af3bd0633f463472e43c7c01bdbd7af168fa75 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Fri, 28 Nov 2014 18:55:07 +0800 Subject: [PATCH 6/7] resolve $ref in PathItem --- pyswagger/scanner/v2_0/resolve.py | 40 +++++++++++++-- pyswagger/tests/data/v2_0/resolve/__init__.py | 0 .../data/v2_0/resolve/path_item/swagger.json | 50 +++++++++++++++++++ pyswagger/tests/v2_0/test_resolve.py | 25 ++++++++++ 4 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 pyswagger/tests/data/v2_0/resolve/__init__.py create mode 100644 pyswagger/tests/data/v2_0/resolve/path_item/swagger.json create mode 100644 pyswagger/tests/v2_0/test_resolve.py diff --git a/pyswagger/scanner/v2_0/resolve.py b/pyswagger/scanner/v2_0/resolve.py index 08a6367..ffe4e71 100644 --- a/pyswagger/scanner/v2_0/resolve.py +++ b/pyswagger/scanner/v2_0/resolve.py @@ -10,12 +10,17 @@ # TODO: test case +# TODO: cyclic detection + +def is_resolved(obj): + return getattr(obj, '$ref') == None or obj.ref_obj != None def _resolve(obj, app, prefix): - r = getattr(obj, '$ref') - if r == None: + if is_resolved(obj): return + r = getattr(obj, '$ref') + try: ro = app.resolve(r) except Exception: @@ -28,6 +33,23 @@ def _resolve(obj, app, prefix): obj.update_field('ref_obj', ro) +def _merge(obj, app, prefix): + """ resolve $ref as ref_obj, and merge ref_obj to self. + This operation should be carried in a cascade manner. + """ + + cur = obj + to_resolve = [] + while not is_resolved(cur): + _resolve(cur, app, prefix) + + to_resolve.append(cur) + cur = cur.ref_obj if cur.ref_obj else cur + + while (len(to_resolve)): + o = to_resolve.pop() + o.merge(o.ref_obj) + class Resolve(object): """ pre-resolve '$ref' """ @@ -35,16 +57,26 @@ class Resolve(object): class Disp(Dispatcher): pass - @Disp.register([Schema, Parameter, Response, PathItem]) + @Disp.register([Schema]) def _schema(self, _, obj, app): _resolve(obj, app, '#/definitions') + @Disp.register([Parameter]) def _parameter(self, _, obj, app): _resolve(obj, app, '#/parameters') + @Disp.register([Response]) def _response(self, _, obj, app): _resolve(obj, app, '#/responses') + @Disp.register([PathItem]) def _path_item(self, _, obj, app): - _resolve(obj, app, '#/paths') + + # $ref in PathItem is 'merge', not 'replace' + # we need to merge properties of others if missing + # in current object. + + # TODO: test case + _merge(obj, app, '#/paths') + diff --git a/pyswagger/tests/data/v2_0/resolve/__init__.py b/pyswagger/tests/data/v2_0/resolve/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json b/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json new file mode 100644 index 0000000..8786d7b --- /dev/null +++ b/pyswagger/tests/data/v2_0/resolve/path_item/swagger.json @@ -0,0 +1,50 @@ +{ + "swagger":"2.0", + "host":"http://test.com", + "basePath":"/v1", + "paths":{ + "/a":{ + "get":{ + "operationId":"a.get", + "responses":{ + "default":{ + "description":"void" + } + } + }, + "$ref":"#/paths/~1b" + }, + "/b":{ + "get":{ + "operationId":"b.get", + "responses":{ + "default":{ + "description":"void" + } + } + }, + "$ref":"#/paths/~1c" + }, + "/c":{ + "put":{ + "operationId":"c.put", + "responses":{ + "default":{ + "description":"void" + } + } + }, + "$ref":"#/paths/~1d" + }, + "/d":{ + "post":{ + "operationId":"d.post", + "responses":{ + "default":{ + "description":"void" + } + } + } + } + } +} diff --git a/pyswagger/tests/v2_0/test_resolve.py b/pyswagger/tests/v2_0/test_resolve.py new file mode 100644 index 0000000..779b286 --- /dev/null +++ b/pyswagger/tests/v2_0/test_resolve.py @@ -0,0 +1,25 @@ +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 """ + + @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): + """ make sure PathItem is correctly merged """ + a = self.app.resolve(utils.jp_compose('/a', '#/paths')) + + 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') From e3ab82252947db3b8f31f693cc61dadabeb06f11 Mon Sep 17 00:00:00 2001 From: "mission.liao" Date: Sun, 30 Nov 2014 00:42:41 +0800 Subject: [PATCH 7/7] fix bugs --- pyswagger/base.py | 5 +++-- pyswagger/tests/test_base.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyswagger/base.py b/pyswagger/base.py index 791d44d..cca0adf 100644 --- a/pyswagger/base.py +++ b/pyswagger/base.py @@ -235,11 +235,12 @@ def resolve(self, ts): return obj def merge(self, other): - """ merge properties from other object + """ merge properties from other object, + only merge from 'not None' to 'None'. """ for name, _ in self.__swagger_fields__: v = getattr(other, name) - if v and getattr(self, name) == None: + if v != None and getattr(self, name) == None: if isinstance(v, weakref.ProxyTypes): # TODO: test case self.update_field(name, v) diff --git a/pyswagger/tests/test_base.py b/pyswagger/tests/test_base.py index db301d8..d123ac5 100644 --- a/pyswagger/tests/test_base.py +++ b/pyswagger/tests/test_base.py @@ -101,10 +101,11 @@ def test_merge(self): 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.assertEqaul(o2.f, '') + self.assertEqual(o2.f, '') self.assertTrue(isinstance(o2.d, ChildObj)) self.assertTrue(isinstance(o2.d, weakref.ProxyTypes))