-
Notifications
You must be signed in to change notification settings - Fork 309
/
json.py
425 lines (324 loc) · 14.7 KB
/
json.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#
# spyne - Copyright (C) Spyne contributors.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
#
"""The ``spyne.protocol.json`` package contains the Json-related protocols.
Currently, only :class:`spyne.protocol.json.JsonDocument` is supported.
Initially released in 2.8.0-rc.
Missing Types
=============
The JSON standard does not define every type that Spyne supports. These include
Date/Time types as well as arbitrary-length integers and arbitrary-precision
decimals. Integers are parsed to ``int``\s or ``long``\s seamlessly but
``Decimal``\s are only parsed correctly when they come off as strings.
While it's possible to e.g. (de)serialize floats to ``Decimal``\s by adding
hooks to ``parse_float`` [#]_ (and convert later as necessary), such
customizations apply to the whole incoming document which pretty much messes up
``AnyDict`` serialization and deserialization.
It also wasn't possible to work with ``object_pairs_hook`` as Spyne's parsing
is always "from outside to inside" whereas ``object_pairs_hook`` is passed
``dict``\s basically in any order "from inside to outside".
.. [#] http://docs.python.org/2/library/json.html#json.loads
"""
from __future__ import absolute_import
import logging
logger = logging.getLogger(__name__)
from itertools import chain
from spyne.util import six
try:
import simplejson as json
from simplejson.decoder import JSONDecodeError
except ImportError:
import json
JSONDecodeError = ValueError
from spyne.error import ValidationError
from spyne.error import ResourceNotFoundError
from spyne.model.binary import BINARY_ENCODING_BASE64
from spyne.model.primitive import Date
from spyne.model.primitive import Time
from spyne.model.primitive import DateTime
from spyne.model.primitive import Double
from spyne.model.primitive import Integer
from spyne.model.primitive import Boolean
from spyne.model.fault import Fault
from spyne.protocol.dictdoc import HierDictDocument
# TODO: use this as default
class JsonEncoder(json.JSONEncoder):
def default(self, o):
try:
return super(JsonEncoder, self).default(o)
except TypeError as e:
# if json can't serialize it, it's possibly a generator. If not,
# additional hacks are welcome :)
if logger.level == logging.DEBUG:
logger.exception(e)
return list(o)
NON_NUMBER_TYPES = tuple({list, dict, six.text_type, six.binary_type})
class JsonDocument(HierDictDocument):
"""An implementation of the json protocol that uses simplejson package when
available, json package otherwise.
:param ignore_wrappers: Does not serialize wrapper objects.
:param complex_as: One of (list, dict). When list, the complex objects are
serialized to a list of values instead of a dict of key/value pairs.
"""
mime_type = 'application/json'
text_based = True
type = set(HierDictDocument.type)
type.add('json')
default_binary_encoding = BINARY_ENCODING_BASE64
# flags used just for tests
_decimal_as_string = True
def __init__(self, app=None, validator=None, mime_type=None,
ignore_uncap=False,
# DictDocument specific
ignore_wrappers=True, complex_as=dict, ordered=False,
default_string_encoding=None, polymorphic=False,
**kwargs):
super(JsonDocument, self).__init__(app, validator, mime_type, ignore_uncap,
ignore_wrappers, complex_as, ordered, polymorphic)
# this is needed when we're overriding a regular instance attribute
# with a property.
self.__message = HierDictDocument.__getattribute__(self, 'message')
self._from_unicode_handlers[Double] = self._ret_number
self._from_unicode_handlers[Boolean] = self._ret_bool
self._from_unicode_handlers[Integer] = self._ret_number
self._to_unicode_handlers[Double] = self._ret
self._to_unicode_handlers[Boolean] = self._ret
self._to_unicode_handlers[Integer] = self._ret
self.default_string_encoding = default_string_encoding
self.kwargs = kwargs
def _ret(self, cls, value):
return value
def _ret_number(self, cls, value):
if isinstance(value, NON_NUMBER_TYPES):
raise ValidationError(value)
if value in (True, False):
return int(value)
return value
def _ret_bool(self, cls, value):
if value is None or value in (True, False):
return value
raise ValidationError(value)
def validate(self, key, cls, val):
super(JsonDocument, self).validate(key, cls, val)
if issubclass(cls, (DateTime, Date, Time)) and not (
isinstance(val, six.string_types) and
cls.validate_string(cls, val)):
raise ValidationError(key, val)
@property
def message(self):
return self.__message
@message.setter
def message(self, val):
if val is self.RESPONSE and not ('cls' in self.kwargs):
self.kwargs['cls'] = JsonEncoder
self.__message = val
def create_in_document(self, ctx, in_string_encoding=None):
"""Sets ``ctx.in_document`` using ``ctx.in_string``."""
try:
in_string = b''.join(ctx.in_string)
if not isinstance(in_string, six.text_type):
if in_string_encoding is None:
in_string_encoding = self.default_string_encoding
if in_string_encoding is not None:
in_string = in_string.decode(in_string_encoding)
ctx.in_document = json.loads(in_string, **self.kwargs)
except JSONDecodeError as e:
raise Fault('Client.JsonDecodeError', repr(e))
def create_out_string(self, ctx, out_string_encoding='utf8'):
"""Sets ``ctx.out_string`` using ``ctx.out_document``."""
if out_string_encoding is None:
ctx.out_string = (json.dumps(o, **self.kwargs)
for o in ctx.out_document)
else:
ctx.out_string = (
json.dumps(o, **self.kwargs).encode(out_string_encoding)
for o in ctx.out_document)
# Continuation of http://stackoverflow.com/a/24184379/1520211
class HybridHttpJsonDocument(JsonDocument):
"""This protocol lets you have the method name as the last fragment in the
request url. Eg. instead of sending a HTTP POST request to
http://api.endpoint/json/
containing: ::
{
"method_name": {
"arg1" : 42,
"arg2" : "foo"
}
}
you will have to send the request to
http://api.endpoint/json/method_name
containing: ::
{
"arg1" : 42,
"arg2" : "foo"
}
Part of request data comes from HTTP and part of it comes from Json, hence
the name.
"""
def create_in_document(self, ctx, in_string_encoding=None):
super(HybridHttpJsonDocument, self).create_in_document(ctx)
url_fragment = ctx.transport.get_path().split('/')[-1]
ctx.in_document = {url_fragment: ctx.in_document}
class JsonP(JsonDocument):
"""The JsonP protocol puts the reponse document inside a designated
javascript function call. The input protocol is identical to the
JsonDocument protocol.
:param callback_name: The name of the function call that will wrapp all
response documents.
For other arguents, see :class:`spyne.protocol.json.JsonDocument`.
"""
type = set(HierDictDocument.type)
type.add('jsonp')
def __init__(self, callback_name, *args, **kwargs):
super(JsonP, self).__init__(*args, **kwargs)
self.callback_name = callback_name
def create_out_string(self, ctx, out_string_encoding='utf8'):
super(JsonP, self).create_out_string(ctx,
out_string_encoding=out_string_encoding)
if out_string_encoding is None:
ctx.out_string = chain(
(self.callback_name, '('),
ctx.out_string,
(');',),
)
else:
ctx.out_string = chain(
[self.callback_name.encode(out_string_encoding), b'('],
ctx.out_string,
[b');'],
)
class _SpyneJsonRpc1(JsonDocument):
version = 1
VERSION = 'ver'
BODY = 'body'
HEAD = 'head'
FAULT = 'fault'
def decompose_incoming_envelope(self, ctx, message=JsonDocument.REQUEST):
indoc = ctx.in_document
if not isinstance(indoc, dict):
raise ValidationError("Invalid Request")
ver = indoc.get(self.VERSION)
if ver is None:
raise ValidationError("Unknown Version")
body = indoc.get(self.BODY)
err = indoc.get(self.FAULT)
if body is None and err is None:
raise ValidationError("Request data not found")
ctx.protocol.error = False
if err is not None:
ctx.in_body_doc = err
ctx.protocol.error = True
else:
if not isinstance(body, dict):
raise ValidationError("Request body not found")
if not len(body) == 1:
raise ValidationError("Need len(body) == 1")
ctx.in_header_doc = indoc.get(self.HEAD)
if not isinstance(ctx.in_header_doc, list):
ctx.in_header_doc = [ctx.in_header_doc]
(ctx.method_request_string,ctx.in_body_doc), = body.items()
def deserialize(self, ctx, message):
assert message in (self.REQUEST, self.RESPONSE)
self.event_manager.fire_event('before_deserialize', ctx)
if ctx.descriptor is None:
raise ResourceNotFoundError(ctx.method_request_string)
if ctx.protocol.error:
ctx.in_object = None
ctx.in_error = self._doc_to_object(ctx, Fault, ctx.in_body_doc)
else:
if message is self.REQUEST:
header_class = ctx.descriptor.in_header
body_class = ctx.descriptor.in_message
elif message is self.RESPONSE:
header_class = ctx.descriptor.out_header
body_class = ctx.descriptor.out_message
# decode header objects
if (ctx.in_header_doc is not None and header_class is not None):
headers = [None] * len(header_class)
for i, (header_doc, head_class) in enumerate(
zip(ctx.in_header_doc, header_class)):
if header_doc is not None and i < len(header_doc):
headers[i] = self._doc_to_object(ctx, head_class,
header_doc)
if len(headers) == 1:
ctx.in_header = headers[0]
else:
ctx.in_header = headers
# decode method arguments
if ctx.in_body_doc is None:
ctx.in_object = [None] * len(body_class._type_info)
else:
ctx.in_object = self._doc_to_object(ctx, body_class,
ctx.in_body_doc)
self.event_manager.fire_event('after_deserialize', ctx)
def serialize(self, ctx, message):
assert message in (self.REQUEST, self.RESPONSE)
self.event_manager.fire_event('before_serialize', ctx)
# construct the soap response, and serialize it
nsmap = self.app.interface.nsmap
ctx.out_document = {
"ver": self.version,
}
if ctx.out_error is not None:
ctx.out_document[self.FAULT] = Fault.to_dict(Fault, ctx.out_error)
else:
if message is self.REQUEST:
header_message_class = ctx.descriptor.in_header
body_message_class = ctx.descriptor.in_message
elif message is self.RESPONSE:
header_message_class = ctx.descriptor.out_header
body_message_class = ctx.descriptor.out_message
# assign raw result to its wrapper, result_message
out_type_info = body_message_class._type_info
out_object = body_message_class()
keys = iter(out_type_info)
values = iter(ctx.out_object)
while True:
try:
k = next(keys)
except StopIteration:
break
try:
v = next(values)
except StopIteration:
v = None
setattr(out_object, k, v)
ctx.out_document[self.BODY] = ctx.out_body_doc = \
self._object_to_doc(body_message_class, out_object)
# header
if ctx.out_header is not None and header_message_class is not None:
if isinstance(ctx.out_header, (list, tuple)):
out_headers = ctx.out_header
else:
out_headers = (ctx.out_header,)
ctx.out_header_doc = out_header_doc = []
for header_class, out_header in zip(header_message_class,
out_headers):
out_header_doc.append(self._object_to_doc(header_class,
out_header))
if len(out_header_doc) > 1:
ctx.out_document[self.HEAD] = out_header_doc
else:
ctx.out_document[self.HEAD] = out_header_doc[0]
self.event_manager.fire_event('after_serialize', ctx)
_json_rpc_flavors = {
'spyne': _SpyneJsonRpc1
}
def JsonRpc(flavour, *args, **kwargs):
assert flavour in _json_rpc_flavors, "Unknown JsonRpc flavor. " \
"Accepted ones are: %r" % tuple(_json_rpc_flavors)
return _json_rpc_flavors[flavour](*args, **kwargs)