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:
parent
569ee6c5f3
commit
40f4a8181d
@ -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
|
||||
"""
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user