jsonrpc: Wrapper takes a mapping between mounts and objects

This is analogous to how the inspector takes a mapping between mounts and
classes.
This commit is contained in:
Stein Magnus Jodal 2012-11-25 08:01:24 +01:00
parent 569ee6c5f3
commit 40f4a8181d
2 changed files with 77 additions and 26 deletions

View File

@ -9,20 +9,38 @@ import pykka
class JsonRpcWrapper(object):
"""
Wraps an object and makes it accessible through JSON-RPC 2.0 messaging.
Wrap objects and make them accessible through JSON-RPC 2.0 messaging.
This class takes responsibility of communicating with the object and
This class takes responsibility of communicating with the objects 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
Only the public methods of the objects 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.
To expose a single object, add it to the objects mapping using the empty
string as the key::
jrw = JsonRpcWrapper(objects={'': my_object})
To expose multiple objects, add them all to the objects mapping. The key in
the mapping is used as the object's mounting point in the exposed API::
jrw = JsonRpcWrapper(objects={
'': foo,
'hello': lambda: 'Hello, world!',
'abc': abc,
})
This will create the following mapping between JSON-RPC 2.0 method names
and Python callables::
bar -> foo.bar()
baz -> foo.baz()
hello -> lambda
abc.def -> abc.def()
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.
@ -30,7 +48,9 @@ class JsonRpcWrapper(object):
For further details on the JSON-RPC 2.0 spec, see
http://www.jsonrpc.org/specification
:param obj: object to be exposed
:param objects: mapping between mounting points and exposed functions or
class instances
:type objects: dict
: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`
@ -38,11 +58,33 @@ class JsonRpcWrapper(object):
method :meth:`default` implemented
"""
def __init__(self, obj, decoders=None, encoders=None):
self.obj = obj
def __init__(self, objects, decoders=None, encoders=None):
self.obj = self._build_exported_object(objects)
self.decoder = get_combined_json_decoder(decoders or [])
self.encoder = get_combined_json_encoder(encoders or [])
def _build_exported_object(self, objects):
class EmptyObject(object):
pass
if '' in objects:
exported_object = objects['']
else:
exported_object = EmptyObject()
mounts = sorted(objects.keys(), key=lambda x: len(x))
for mount in mounts:
parent = exported_object
path = mount.split('.')
for part in path[:-1]:
if not hasattr(parent, part):
setattr(parent, part, EmptyObject())
parent = getattr(parent, part)
if path[-1]:
setattr(parent, path[-1], objects[mount])
return exported_object
def handle_json(self, request):
"""
Handles an incoming request encoded as a JSON string.
@ -250,10 +292,24 @@ def get_combined_json_encoder(encoders):
class JsonRpcInspector(object):
"""
Inspects a group of objects to create a description of what methods they
can expose over JSON-RPC 2.0.
Inspects a group of classes and functions to create a description of what
methods they can expose over JSON-RPC 2.0.
:param objects: mapping between mounts and exposed classes
To inspect a single class, add it to the objects mapping using the empty
string as the key::
jri = JsonRpcInspector(objects={'': MyClas})
To inspect multiple classes, add them all to the objects mapping. The key
in the mapping is used as the classes' mounting point in the exposed API::
jri = JsonRpcInspector(objects={
'': Foo,
'hello': lambda: 'Hello, world!',
'abc': Abc,
})
:param objects: mapping between mounts and exposed functions or classes
:type objects: dict
"""

View File

@ -12,10 +12,6 @@ from mopidy.utils import jsonrpc
from tests import unittest
class ExportedObject(object):
pass
class Calculator(object):
def model(self):
return 'TI83'
@ -45,13 +41,12 @@ class JsonRpcTestBase(unittest.TestCase):
self.backend = dummy.DummyBackend.start(audio=None).proxy()
self.core = core.Core.start(backends=[self.backend]).proxy()
exported = ExportedObject()
exported.hello = lambda: 'Hello, world!'
exported.core = self.core
exported.calculator = Calculator()
self.jrw = jsonrpc.JsonRpcWrapper(
obj=exported,
objects={
'hello': lambda: 'Hello, world!',
'core': self.core,
'': Calculator(),
},
encoders=[models.ModelJSONEncoder],
decoders=[models.model_json_decoder])
@ -135,10 +130,10 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
self.assertNotIn('error', response)
self.assertEqual(response['result'], 'Hello, world!')
def test_call_method_on_plain_object(self):
def test_call_method_on_plain_object_as_root(self):
request = {
'jsonrpc': '2.0',
'method': 'calculator.model',
'method': 'model',
'id': 1,
}
response = self.jrw.handle_data(request)
@ -151,7 +146,7 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
def test_call_method_which_returns_dict_from_plain_object(self):
request = {
'jsonrpc': '2.0',
'method': 'calculator.describe',
'method': 'describe',
'id': 1,
}
response = self.jrw.handle_data(request)
@ -400,7 +395,7 @@ class JsonRpcSingleCommandErrorTest(JsonRpcTestBase):
def test_private_method_causes_unknown_method_error(self):
request = {
'jsonrpc': '2.0',
'method': 'calculator._secret',
'method': '_secret',
'id': 1,
}
response = self.jrw.handle_data(request)