mopidy/mopidy/utils/jsonrpc.py
2012-11-22 15:31:03 +01:00

248 lines
7.8 KiB
Python

from __future__ import unicode_literals
import json
import traceback
import pykka
class JsonRpcWrapper(object):
"""
Wraps an object and makes it accessible through JSON-RPC 2.0 messaging.
This class takes responsibility of communicating with the object and
processing of JSON-RPC 2.0 messages. The transport of the messages over
HTTP, WebSocket, TCP, or whatever is of no concern to this class.
Only the object's public methods will be exposed. Attributes are not
exposed by themself, but public methods on public attributes are exposed,
using dotted paths from the exposed object to the method at the end of the
path.
To expose multiple objects, simply create a new "parent" object and assign
the other objects you want to expose to attributes on the "parent" object.
You then wrap the "parent" object.
If a method returns a :class:`pykka.Future`, the future will be completed
and its value unwrapped before the JSON-RPC wrapper returns the response.
For further details on the JSON-RPC 2.0 spec, see
http://www.jsonrpc.org/specification
:param obj: object to be exposed
:param decoders: object builders to be used by :func`json.loads`
:type decoders: list of functions taking a dict and returning a dict
:param encoders: object serializers to be used by :func:`json.dumps`
:type encoders: list of :class:`json.JSONEncoder` subclasses with the
method :meth:`default` implemented
"""
def __init__(self, obj, decoders=None, encoders=None):
self.obj = obj
self.decoder = get_combined_json_decoder(decoders or [])
self.encoder = get_combined_json_encoder(encoders or [])
def handle_json(self, request):
"""
Handles an incoming request encoded as a JSON string.
Returns a response as a JSON string for commands, and :class:`None` for
notifications.
:param request: the serialized JSON-RPC request
:type request: string
:rtype: string or :class:`None`
"""
try:
request = json.loads(request, object_hook=self.decoder)
except ValueError:
response = JsonRpcParseError().get_response()
else:
response = self.handle_data(request)
if response is None:
return None
return json.dumps(response, cls=self.encoder)
def handle_data(self, request):
"""
Handles an incoming request in the form of a Python data structure.
Returns a Python data structure for commands, or a :class:`None` for
notifications.
:param request: the unserialized JSON-RPC request
:type request: dict
:rtype: dict, list, or :class:`None`
"""
if isinstance(request, list):
return self._handle_batch(request)
else:
return self._handle_single_request(request)
def _handle_batch(self, requests):
if not requests:
return JsonRpcInvalidRequestError(
data='Batch list cannot be empty').get_response()
responses = []
for request in requests:
response = self._handle_single_request(request)
if response:
responses.append(response)
return responses or None
def _handle_single_request(self, request):
try:
self._validate_request(request)
args, kwargs = self._get_params(request)
except JsonRpcInvalidRequestError as error:
return error.get_response()
try:
method = self._get_method(request['method'])
try:
result = method(*args, **kwargs)
if self._is_notification(request):
return None
if self._is_future(result):
result = result.get()
return {
'jsonrpc': '2.0',
'id': request['id'],
'result': result,
}
except TypeError as error:
raise JsonRpcInvalidParamsError(data={
'type': error.__class__.__name__,
'message': unicode(error),
'traceback': traceback.format_exc(),
})
except Exception as error:
raise JsonRpcApplicationError(data={
'type': error.__class__.__name__,
'message': unicode(error),
'traceback': traceback.format_exc(),
})
except JsonRpcError as error:
if self._is_notification(request):
return None
return error.get_response(request['id'])
def _validate_request(self, request):
if not isinstance(request, dict):
raise JsonRpcInvalidRequestError(
data='Request must be an object')
if not 'jsonrpc' in request:
raise JsonRpcInvalidRequestError(
data='"jsonrpc" member must be included')
if request['jsonrpc'] != '2.0':
raise JsonRpcInvalidRequestError(
data='"jsonrpc" value must be "2.0"')
if not 'method' in request:
raise JsonRpcInvalidRequestError(
data='"method" member must be included')
if not isinstance(request['method'], unicode):
raise JsonRpcInvalidRequestError(
data='"method" must be a string')
def _get_params(self, request):
if not 'params' in request:
return [], {}
params = request['params']
if isinstance(params, list):
return params, {}
elif isinstance(params, dict):
return [], params
else:
raise JsonRpcInvalidRequestError(
data='"params", if given, must be an array or an object')
def _get_method(self, name):
try:
path = name.split('.')
this = self.obj
for part in path:
if part.startswith('_'):
raise AttributeError
this = getattr(this, part)
return this
except (AttributeError, KeyError):
raise JsonRpcMethodNotFoundError()
def _is_notification(self, request):
return 'id' not in request
def _is_future(self, result):
return isinstance(result, pykka.Future)
class JsonRpcError(Exception):
code = -32000
message = 'Unspecified server error'
def __init__(self, data=None):
self.data = data
def get_response(self, request_id=None):
response = {
'jsonrpc': '2.0',
'id': request_id,
'error': {
'code': self.code,
'message': self.message,
},
}
if self.data:
response['error']['data'] = self.data
return response
class JsonRpcParseError(JsonRpcError):
code = -32700
message = 'Parse error'
class JsonRpcInvalidRequestError(JsonRpcError):
code = -32600
message = 'Invalid Request'
class JsonRpcMethodNotFoundError(JsonRpcError):
code = -32601
message = 'Method not found'
class JsonRpcInvalidParamsError(JsonRpcError):
code = -32602
message = 'Invalid params'
class JsonRpcApplicationError(JsonRpcError):
code = 0
message = 'Application error'
def get_combined_json_decoder(decoders):
def decode(dct):
for decoder in decoders:
dct = decoder(dct)
return dct
return decode
def get_combined_json_encoder(encoders):
class JsonRpcEncoder(json.JSONEncoder):
def default(self, obj):
for encoder in encoders:
try:
return encoder().default(obj)
except TypeError:
pass # Try next encoder
return json.JSONEncoder.default(self, obj)
return JsonRpcEncoder