Skip to content

Commit 0857dfb

Browse files
committed
Merge pull request #57 from mission-liao/external_reference
external reference to documents containing any valid json/yaml struct
2 parents 8725c53 + b42f0fa commit 0857dfb

25 files changed

+627
-269
lines changed

pyswagger/core.py

Lines changed: 104 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from __future__ import absolute_import
22
from .getter import UrlGetter, LocalGetter
3+
from .resolve import SwaggerResolver
34
from .primitives import SwaggerPrimitive
45
from .spec.v1_2.parser import ResourceListContext
56
from .spec.v2_0.parser import SwaggerContext
67
from .spec.v2_0.objects import Operation
8+
from .spec.base import BaseObj
79
from .scan import Scanner
810
from .scanner import TypeReduce, CycleDetector
911
from .scanner.v1_2 import Upgrade
10-
from .scanner.v2_0 import AssignParent, Resolve, PatchObject, YamlFixer, Aggregate
12+
from .scanner.v2_0 import AssignParent, Merge, Resolve, PatchObject, YamlFixer, Aggregate, NormalizeRef
1113
from pyswagger import utils, errs, consts
12-
import inspect
1314
import base64
1415
import six
1516
import weakref
16-
import os
1717
import logging
1818

1919

@@ -33,11 +33,10 @@ class SwaggerApp(object):
3333
sc_path: ('/', '#/paths')
3434
}
3535

36-
def __init__(self, url=None, app_cache=None, url_load_hook=None, sep=consts.private.SCOPE_SEPARATOR, prim=None):
36+
def __init__(self, url=None, url_load_hook=None, sep=consts.private.SCOPE_SEPARATOR, prim=None):
3737
""" constructor
3838
3939
:param url str: url of swagger.json
40-
:param dict app_cache: a url map shared by SwaggerApp(s), mapping from url to SwaggerApp
4140
:param func url_load_hook: a way to redirect url to a accessible place. for self testing.
4241
:param sep str: separator used by pyswager.utils.ScopeDict
4342
:param prim pyswagger.primitives.SwaggerPrimitive: factory for primitives in Swagger.
@@ -54,15 +53,14 @@ def __init__(self, url=None, app_cache=None, url_load_hook=None, sep=consts.priv
5453
self.__schemes = []
5554
self.__url=url
5655

57-
# a map from url to SwaggerApp
58-
self.__app_cache = {} if app_cache == None else app_cache
56+
# a map from json-reference to
57+
# - spec.BaseObj
58+
# - a map from json-pointer to spec.BaseObj
59+
self.__objs = {}
60+
self.__resolver = SwaggerResolver(url_load_hook)
5961
# keep a string reference to SwaggerApp when resolve
6062
self.__strong_refs = []
6163

62-
# things to make unittest easier,
63-
# all urls to load json would go through this hook
64-
self.__url_load_hook = url_load_hook
65-
6664
# allow init App-wised SCOPE_SEPARATOR
6765
self.__sep = sep
6866

@@ -139,12 +137,6 @@ def url(self):
139137
"""
140138
return self.__url
141139

142-
@property
143-
def _app_cache(self):
144-
""" internal usage
145-
"""
146-
return self.__app_cache
147-
148140
@property
149141
def prim_factory(self):
150142
""" primitive factory used by this app
@@ -153,41 +145,18 @@ def prim_factory(self):
153145
"""
154146
return self.__prim
155147

156-
def _load_obj(self, url, getter=None, parser=None):
157-
"""
148+
def load_obj(self, jref, getter=None, parser=None):
149+
""" load a object(those in spec._version_.objects) from a JSON reference.
158150
"""
159-
if url in self.__app_cache:
160-
logger.info('{0} hit cache'.format(url))
161-
162-
# look into cache first
163-
return
164-
165-
# apply hook when use this url to load
166-
# note that we didn't cache SwaggerApp with this local_url
167-
local_url = url if not self.__url_load_hook else self.__url_load_hook(url)
168-
169-
logger.info('{0} patch to {1}'.format(url, local_url))
170-
171-
if not getter:
172-
getter = UrlGetter
173-
p = six.moves.urllib.parse.urlparse(local_url)
174-
if p.scheme == 'file' and p.path:
175-
getter = LocalGetter(os.path.join(p.netloc, p.path))
176-
177-
if inspect.isclass(getter):
178-
# default initialization is passing the url
179-
# you can override this behavior by passing an
180-
# initialized getter object.
181-
getter = getter(local_url)
151+
obj = self.__resolver.resolve(jref, getter)
182152

183153
# get root document to check its swagger version.
184-
obj, _ = six.advance_iterator(getter)
185154
tmp = {'_tmp_': {}}
186155
version = utils.get_swagger_version(obj)
187156
if version == '1.2':
188157
# swagger 1.2
189158
with ResourceListContext(tmp, '_tmp_') as ctx:
190-
ctx.parse(getter, obj)
159+
ctx.parse(obj, jref, self.__resolver, getter)
191160
elif version == '2.0':
192161
# swagger 2.0
193162
with SwaggerContext(tmp, '_tmp_') as ctx:
@@ -198,13 +167,55 @@ def _load_obj(self, url, getter=None, parser=None):
198167

199168
version = tmp['_tmp_'].__swagger_version__ if hasattr(tmp['_tmp_'], '__swagger_version__') else version
200169
else:
201-
raise NotImplementedError('Unsupported Swagger Version: {0} from {1}'.format(version, url))
170+
raise NotImplementedError('Unsupported Swagger Version: {0} from {1}'.format(version, jref))
171+
172+
if not tmp['_tmp_']:
173+
raise Exception('Unable to parse object from {0}'.format(jref))
202174

203175
logger.info('version: {0}'.format(version))
204176

205-
self.__app_cache[url] = weakref.proxy(self) # avoid circular reference
206-
self.__version = version
207-
self.__raw = tmp['_tmp_']
177+
return tmp['_tmp_'], version
178+
179+
def prepare_obj(self, obj, jref):
180+
""" basic preparation of an object(those in sepc._version_.objects),
181+
and cache the 'prepared' object.
182+
"""
183+
if not obj:
184+
raise Exception('unexpected, passing {0}:{1} to prepare'.format(obj, jref))
185+
186+
s = Scanner(self)
187+
if self.version == '1.2':
188+
# upgrade from 1.2 to 2.0
189+
converter = Upgrade(self.__sep)
190+
s.scan(root=obj, route=[converter])
191+
obj = converter.swagger
192+
193+
if not obj:
194+
raise Exception('unable to upgrade from 1.2: {0}'.format(jref))
195+
196+
s.scan(root=obj, route=[AssignParent()])
197+
198+
# normalize $ref
199+
url, jp = utils.jr_split(jref)
200+
s.scan(root=obj, route=[NormalizeRef(url)])
201+
# fix for yaml that treat response code as number
202+
s.scan(root=obj, route=[YamlFixer()], leaves=[Operation])
203+
204+
# cache this object
205+
if url not in self.__objs:
206+
if jp == '#':
207+
self.__objs[url] = obj
208+
else:
209+
self.__objs[url] = {jp: obj}
210+
else:
211+
if not isinstance(self.__objs[url], dict):
212+
raise Exception('it should be able to resolve with BaseObj')
213+
self.__objs[url].update({jp: obj})
214+
215+
# pre resolve Schema Object
216+
# note: make sure this object is cached before using 'Resolve' scanner
217+
s.scan(root=obj, route=[Resolve()])
218+
return obj
208219

209220
def _validate(self):
210221
""" check if this Swagger API valid or not.
@@ -232,44 +243,8 @@ def _validate(self):
232243
s.scan(route=[v], root=self.__raw)
233244
return v.errs
234245

235-
def _prepare_obj(self, strict=True):
236-
"""
237-
"""
238-
if self.__root:
239-
return
240-
241-
s = Scanner(self)
242-
self.validate(strict=strict)
243-
244-
if self.version == '1.2':
245-
converter = Upgrade(self.__sep)
246-
s.scan(root=self.raw, route=[converter])
247-
obj = converter.swagger
248-
249-
# We only have to run this scanner when upgrading from 1.2.
250-
# Mainly because we initial BaseObj via NullContext
251-
s.scan(root=obj, route=[AssignParent()])
252-
253-
self.__root = obj
254-
elif self.version == '2.0':
255-
s.scan(root=self.raw, route=[YamlFixer()], leaves=[Operation])
256-
self.__root = self.raw
257-
else:
258-
raise NotImplementedError('Unsupported Version: {0}'.format(self.__version))
259-
260-
if hasattr(self.__root, 'schemes') and self.__root.schemes:
261-
if len(self.__root.schemes) > 0:
262-
self.__schemes = self.__root.schemes
263-
else:
264-
# extract schemes from the url to load spec
265-
self.__schemes = [six.moves.urlparse(self.__url).schemes]
266-
267-
s.scan(root=self.__root, route=[Resolve()])
268-
s.scan(root=self.__root, route=[PatchObject()])
269-
s.scan(root=self.__root, route=[Aggregate()])
270-
271246
@classmethod
272-
def load(kls, url, getter=None, parser=None, app_cache=None, url_load_hook=None, sep=consts.private.SCOPE_SEPARATOR, prim=None):
247+
def load(kls, url, getter=None, parser=None, url_load_hook=None, sep=consts.private.SCOPE_SEPARATOR, prim=None):
273248
""" load json as a raw SwaggerApp
274249
275250
:param str url: url of path of Swagger API definition
@@ -290,9 +265,10 @@ def load(kls, url, getter=None, parser=None, app_cache=None, url_load_hook=None,
290265
logger.info('load with [{0}]'.format(url))
291266

292267
url = utils.normalize_url(url)
293-
app = kls(url, app_cache=app_cache, url_load_hook=url_load_hook, sep=sep, prim=prim)
294-
295-
app._load_obj(url, getter, parser)
268+
app = kls(url, url_load_hook=url_load_hook, sep=sep, prim=prim)
269+
app.__raw, app.__version = app.load_obj(url, getter=getter, parser=parser)
270+
if app.__version not in ['1.2', '2.0']:
271+
raise NotImplementedError('Unsupported Version: {0}'.format(self.__version))
296272

297273
# update schem if any
298274
p = six.moves.urllib.parse.urlparse(url)
@@ -321,10 +297,22 @@ def prepare(self, strict=True):
321297
:param bool strict: when in strict mode, exception would be raised if not valid.
322298
"""
323299

324-
self._prepare_obj(strict=strict)
300+
self.validate(strict=strict)
301+
self.__root = self.prepare_obj(self.raw, self.__url)
302+
303+
if hasattr(self.__root, 'schemes') and self.__root.schemes:
304+
if len(self.__root.schemes) > 0:
305+
self.__schemes = self.__root.schemes
306+
else:
307+
# extract schemes from the url to load spec
308+
self.__schemes = [six.moves.urlparse(self.__url).schemes]
325309

326-
# reducer for Operation
327310
s = Scanner(self)
311+
s.scan(root=self.__root, route=[Merge()])
312+
s.scan(root=self.__root, route=[PatchObject()])
313+
s.scan(root=self.__root, route=[Aggregate()])
314+
315+
# reducer for Operation
328316
tr = TypeReduce(self.__sep)
329317
cy = CycleDetector()
330318
s.scan(root=self.__root, route=[tr, cy])
@@ -381,28 +369,40 @@ def resolve(self, jref, parser=None):
381369
if jref == None or len(jref) == 0:
382370
raise ValueError('Empty Path is not allowed')
383371

372+
obj = None
384373
url, jp = utils.jr_split(jref)
385374
if url:
386-
if url not in self.__app_cache:
387-
# This loaded SwaggerApp would be kept in app_cache.
388-
app = SwaggerApp.load(url, parser=parser, app_cache=self.__app_cache, url_load_hook=self.__url_load_hook)
389-
app.prepare()
390-
391-
# nothing but only keeping a strong reference of
392-
# loaded SwaggerApp.
393-
self.__strong_refs.append(app)
394-
395-
return self.__app_cache[url].resolve(jp)
375+
# check cacahed object against json reference by
376+
# comparing url first, and find those object prefixed with
377+
# the JSON pointer.
378+
o = self.__objs.get(url, None)
379+
if o:
380+
if isinstance(o, BaseObj):
381+
obj = o.resolve(utils.jp_split(jp)[1:])
382+
elif isinstance(o, dict):
383+
for k, v in six.iteritems(o):
384+
if jp.startswith(k):
385+
obj = v.resolve(utils.jp_split(jp[len(k):])[1:])
386+
break
387+
else:
388+
raise Exception('Unknown Cached Object: {0}'.format(str(type(o))))
396389

397-
if not jp.startswith('#'):
398-
raise ValueError('Invalid Path, root element should be \'#\', but [{0}]'.format(jref))
390+
# this object is not loaded yet, load it
391+
if obj == None:
392+
obj, _ = self.load_obj(jref, parser=parser)
393+
if obj:
394+
obj = self.prepare_obj(obj, jref)
395+
else:
396+
# a local reference, 'jref' is just a json-pointer
397+
if not jp.startswith('#'):
398+
raise ValueError('Invalid Path, root element should be \'#\', but [{0}]'.format(jref))
399399

400-
obj = self.root.resolve(utils.jp_split(jp)[1:]) # heading element is #, mapping to self.root
400+
obj = self.root.resolve(utils.jp_split(jp)[1:]) # heading element is #, mapping to self.root
401401

402402
if obj == None:
403403
raise ValueError('Unable to resolve path, [{0}]'.format(jref))
404404

405-
if isinstance(obj, (six.string_types, int, list, dict)):
405+
if isinstance(obj, (six.string_types, six.integer_types, list, dict)):
406406
return obj
407407
return weakref.proxy(obj)
408408

0 commit comments

Comments
 (0)