1. Atsushi Odagiri
  2. microapps

Commits

Atsushi Odagiri  committed 62cbbf4

rewrite for JSON-RPC 2.0 spec.

  • Participants
  • Parent commits 66d11c6
  • Branches default

Comments (0)

Files changed (3)

File buildout.cfg

View file
 [buildout]
 parts =
     eggs
+    run
 develop =
     src/jsonrpc
+    src/restfuljson
+    src/dispatcher
+    apps/journaly
 
 [eggs]
 recipe = zc.recipe.egg
 eggs =
+    dispatcher
+    restfuljson
     jsonrpc
     nose
     WebTest
 
-
+[run]
+recipe = zc.recipe.egg:scripts
+eggs = 
+    PasteScript
+    journaly
+arguments = args="serve --reload apps/journaly/devel.ini".split(" ")
+scripts =
+    paster=runner

File src/jsonrpc/jsonrpc/__init__.py

View file
 # -*- coding:utf-8 -*-
+
+"""
+http://groups.google.com/group/json-rpc/web/json-rpc-2-0
+
+errors:
+
+code 	message 	meaning
+-32700 	Parse error 	Invalid JSON was received by the server.
+An error occurred on the server while parsing the JSON text.
+-32600 	Invalid Request 	The JSON sent is not a valid Request object.
+-32601 	Method not found 	The method does not exist / is not available.
+-32602 	Invalid params 	Invalid method parameter(s).
+-32603 	Internal error 	Internal JSON-RPC error.
+-32099 to -32000 	Server error 	Reserved for implementation-defined server-errors.
+
+"""
+PARSE_ERROR = -32700
+INVALID_REQUEST = -32600
+METHOD_NOT_FOUND = -32601
+INVALID_PARAMS = -32602
+INTERNAL_ERROR = -32603
+errors = {}
+errors[PARSE_ERROR] = "Parse Error"
+errors[INVALID_REQUEST] = "Invalid Request"
+errors[METHOD_NOT_FOUND] = "Method Not Found"
+errors[INVALID_PARAMS] = "Invalid Params"
+errors[INTERNAL_ERROR] = "Internal Error"
+
+try:
+    import json
+except ImportError:
+    try:
+        import django.utils.simplejson as json
+    except ImportError:
+        import simplejson as json
+
 import itertools
 from webob import Request, Response
 from webob import exc
-import simplejson as json
-
 
 class JsonRpcApplication(object):
-    def __init__(self, objmap):
-        self.objmap = objmap
+    def __init__(self, methods):
+        self.methods = methods
 
     def process(self, data):
+        if data['jsonrpc'] != "2.0":
+            return {'jsonrpc':'2.0',
+                    'id':data.get('id'),
+                    'error':{'code':INVALID_REQUEST,
+                             'message':errors[INVALID_REQUEST]}}
         if 'method' not in data:
-            raise exc.HTTPBadRequest
-        if data['method'].count('.') != 1:
-            return {
-                'error':{
-                    'message':"invalid method name.('.' is not included or too many.)"
-                    }
-                }
-        objname, methodname = data['method'].split('.')
-        obj = self.objmap[objname]
+            return {'jsonrpc':'2.0',
+                    'id':data.get('id'),
+                    'error':{'code':INVALID_REQUEST,
+                             'message':errors[INVALID_REQUEST]}}
+
+        methodname = data['method']
 
         if methodname.startswith('_'):
-            return {
-                'error':{
-                    'message':methodname + " is not found."
-                    }
-                }
+            return {'jsonrpc':'2.0',
+                    'id':data.get('id'),
+                    'error':{'code':METHOD_NOT_FOUND,
+                             'message':errors[METHOD_NOT_FOUND]}}
 
 
-        if not hasattr(obj, methodname):
-            return {
-                'error':{
-                    'message':methodname + " is not found."
-                    }
-                }
+        if methodname not in self.methods:
+            return {'jsonrpc':'2.0',
+                    'id':data.get('id'),
+                    'error':{'code':METHOD_NOT_FOUND,
+                             'message':errors[METHOD_NOT_FOUND]}}
 
-        method = getattr(obj, methodname)
+
+        method = self.methods[methodname]
         try:
-            result = method(*data.get('params', []))
+            params = data.get('params', [])
+            if isinstance(params, list):
+                result = method(*params)
+            elif isinstance(params, dict):
+                result = method(**params)
+            else:
+                return {'jsonrpc':'2.0',
+                        'id':data.get('id'),
+                        'error':{'code':INVALID_REQUEST,
+                                 'message':errors[INVALID_REQUEST]}}
+            
             resdata = {
-                'errors':[],
+                'jsonrpc':'2.0',
+                'id':data['id'],
                 'result':result,
             }
+            return resdata
         except Exception, e:
-            resdata = {
-                'errors':[str(e)],
-            }
-        return resdata
+            return {'jsonrpc':'2.0',
+                    'id':data.get('id'),
+                    'error':{'code':INTERNAL_ERROR,
+                             'message':errors[INTERNAL_ERROR],
+                             'data':str(e)}}
 
 
-    def methodlist(self, environ, start_response):
-        methods = itertools.chain(*[[n + "." + m 
-                                     for m in o.__class__.__dict__
-                                     if (not m.startswith('_') 
-                                         and callable(getattr(o, m)))]
-                                   for n, o in self.objmap.iteritems()])
-        data = json.dumps(list(methods))
-        res = Response(content_type='application/json',
-                       body=data)
-        return res(environ, start_response)
-            
+
 
     def __call__(self, environ, start_response):
         request = Request(environ)
         if request.method != "POST":
-            return self.methodlist(environ, start_response)
+            raise exc.HTTPMethodNotAllowed
+
         if request.content_type != 'application/json':
             raise exc.HTTPBadRequest(body='Content-type must by application/json')
 
         data = json.loads(request.body)
-
-        resdata = dict([(reqid, self.process(data[reqid]))
-                       for reqid in data])
+        if isinstance(data, dict):
+            resdata = self.process(data)
+        elif isinstance(data, list):
+            resdata = [self.process(d) for d in data]
             
         
         response = Response(content_type="application/json")

File src/jsonrpc/tests/__init__.py

View file
 from jsonrpc import JsonRpcApplication
 from webob import exc
 
-class TestObj(object):
-    def greeting(self, name):
-        return "Hello, %s" % name
+def subtract(minuend, subtrahend):
+    return minuend - subtrahend
 
-    def dummy(self):
-        return "dummy"
+def createapp():
+    app = JsonRpcApplication(dict(subtract=subtract))
+    app = TestApp(app)
+    return app
 
-    def add(self, a, b):
-        return dict(a=a, b=b, added=a+b)
 
-    def _notcallable(self):
-        pass
+app = createapp()
 
+"""
+rpc call with positional parameters:
+"""
 
-def test_refrection():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    res = app.get('/')
-    data =res.json
+def test_positional_params1():
+    """
+    --> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
+    <-- {"jsonrpc": "2.0", "result": 19, "id": 1}
+    """
+    data = {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
+    
+    res = app.post('/', params=json.dumps(data),
+                   extra_environ={'CONTENT_TYPE':'application/json'})
+    assert res.status_int == 200
     print res.body
-    assert len(data) == 3
-    assert 'greeting.greeting' in data
-    assert 'greeting.add' in data
+    data = res.json
+    assert data['jsonrpc'] == '2.0'
+    assert data['result'] == 19
+    assert data['id'] == 1
 
+def test_positional_params2():
+    """
+    --> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
+    
+    <-- {"jsonrpc": "2.0", "result": -19, "id": 2}
+    """
+    data = {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
+    res = app.post('/', params=json.dumps(data),
+                   extra_environ={'CONTENT_TYPE':'application/json'})
+    assert res.status_int == 200
+    print res.body
+    data = res.json
+    assert data['jsonrpc'] == '2.0'
+    assert data['result'] == -19
+    assert data['id'] == 2
 
-def test_greeting():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    data = {
-        "greeting-test":{
-            "method":"greeting.greeting",
-            "params":['aodag'],
-        },
-    }
+
+"""
+rpc call with named parameters:
+"""
+def test_named_params1():
+    """
+    --> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
+    <-- {"jsonrpc": "2.0", "result": 19, "id": 3}
+    """
+    data = {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
     res = app.post('/', params=json.dumps(data),
-            extra_environ={'CONTENT_TYPE':'application/json'})
+                   extra_environ={'CONTENT_TYPE':'application/json'})
+    assert res.status_int == 200
+    print res.body
+    data = res.json
+    assert data['jsonrpc'] == '2.0'
+    assert data['result'] == 19
+    assert data['id'] == 3
 
-    results = res.json
-    assert results['greeting-test']['result'] == "Hello, aodag"
 
-        
-def test_with_params():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    data = {
-        "param-test":{
-            "method":"greeting.add",
-            "params":[1, 2],
-        },
-    }
+def test_named_params2():
+    """
+    --> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
+    
+    <-- {"jsonrpc": "2.0", "result": 19, "id": 4}
+    """
+    data = {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
+    res = app.post('/', params=json.dumps(data),
+                   extra_environ={'CONTENT_TYPE':'application/json'})
+    assert res.status_int == 200
+    print res.body
+    data = res.json
+    assert data['jsonrpc'] == '2.0'
+    assert data['result'] == 19
+    assert data['id'] == 4
 
-    res = app.post('/', params=json.dumps(data),
-            extra_environ={'CONTENT_TYPE':'application/json'})
-    results = res.json['param-test']
 
-    assert results['result']['a'] == 1
-    assert results['result']['b'] == 2
-    assert results['result']['added'] == 3
+"""
+a Notification:
 
-def test_batch_request():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    data = {
-        "param-test1":
-        {
-            "method":"greeting.add",
-            "params":[1, 2],
-        },
-        "param-test2":
-        {
-            "method":"greeting.add",
-            "params":[5, 6],
-        }
-    }
-    res = app.post('/', params=json.dumps(data),
-            extra_environ={'CONTENT_TYPE':'application/json'})
-    results = res.json
+--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}
+--> {"jsonrpc": "2.0", "method": "foobar"}
+"""
 
-def test_content_type():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    try:
-        res = app.post('/')
-    except exc.HTTPBadRequest, e:
-        print e.body
-        assert e.body == 'Content-type must by application/json'
-    
-def test_invalidmethod():
-    app = TestApp(JsonRpcApplication(dict(greeting=TestObj())))
-    data = {
-        "param-test1":
-        {
-            "method":"greeting.add",
-            "params":[1, 2],
-        },
-        "param-test2":
-        {
-            "method":"greeting.add",
-            "params":[5, 6],
-        },
-        'invalid1':{
-            "method":"greeting._notcallable",
-            },
-        'invalid2':{
-            "method":"greeting.notdefined"
-            },
-        'invalid3':{
-            "method":"invalidname"
-            }
-    }
-    res = app.post('/', params=json.dumps(data),
-            extra_environ={'CONTENT_TYPE':'application/json'})
-    results = res.json
-    assert len(results) == 5
-    assert results['invalid1']['error']
-    assert results['invalid2']['error']
-    assert results['invalid3']['error']
+"""
+rpc call of non-existent method:
+
+--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
+<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Procedure not found."}, "id": "1"}
+"""
+
+"""
+rpc call with invalid JSON:
+
+--> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
+<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
+"""
+
+"""
+rpc call with invalid Request object:
+
+--> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
+<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
+"""
+
+"""
+rpc call with invalid Batch:
+
+--> [1,2,3]
+
+<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
+"""
+
+"""
+rpc call Batch:
+
+--> [
+        {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
+        {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
+        {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
+        {"foo": "boo"},
+        {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
+        {"jsonrpc": "2.0", "method": "get_data", "id": "9"} 
+    ]
+
+<-- [
+        {"jsonrpc": "2.0", "result": 7, "id": "1"},
+        {"jsonrpc": "2.0", "result": 19, "id": "2"},
+        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
+        {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, id: "5"},
+        {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
+    ]
+"""
+
+"""
+rpc call Batch, invalid JSON:
+
+--> [ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]
+<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
+
+
+"""