diff --git a/AUTHORS b/AUTHORS index 23e3ba4..064e9d4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -24,3 +24,4 @@ Laurent Mazuel (@lmazuel) Igor Melnyk (@liminspace) Ghislain Antony Vaillant (@ghisvail) Chris Jerdonek (@cjerdonek) +Kacper Kubkowski (@kacper-ka) diff --git a/jsonrpc/exceptions.py b/jsonrpc/exceptions.py index 175ba92..1a7bffa 100644 --- a/jsonrpc/exceptions.py +++ b/jsonrpc/exceptions.py @@ -172,6 +172,13 @@ class JSONRPCInvalidRequestException(JSONRPCException): pass +class JSONRPCInvalidResponseException(JSONRPCException): + + """ Response is not valid.""" + + pass + + class JSONRPCDispatchException(JSONRPCException): """ JSON-RPC Dispatch Exception. diff --git a/jsonrpc/jsonrpc.py b/jsonrpc/jsonrpc.py index f02126e..92c11a9 100644 --- a/jsonrpc/jsonrpc.py +++ b/jsonrpc/jsonrpc.py @@ -26,3 +26,20 @@ def from_data(cls, data): return JSONRPC10Request.from_data(data) else: return JSONRPC20Request.from_data(data) + + +class JSONRPCResponse(JSONSerializable): + + """ JSONRPC Response.""" + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + if isinstance(data, dict) and 'jsonrpc' not in data: + return JSONRPC10Response.from_data(data) + else: + return JSONRPC20Response.from_data(data) diff --git a/jsonrpc/jsonrpc1.py b/jsonrpc/jsonrpc1.py index 3fcbe9b..1a96ce6 100644 --- a/jsonrpc/jsonrpc1.py +++ b/jsonrpc/jsonrpc1.py @@ -1,7 +1,7 @@ from . import six from .base import JSONRPCBaseRequest, JSONRPCBaseResponse -from .exceptions import JSONRPCInvalidRequestException, JSONRPCError +from .exceptions import JSONRPCInvalidRequestException, JSONRPCInvalidResponseException, JSONRPCError class JSONRPC10Request(JSONRPCBaseRequest): @@ -105,6 +105,8 @@ def from_data(cls, data): class JSONRPC10Response(JSONRPCBaseResponse): JSONRPC_VERSION = "1.0" + REQUIRED_FIELDS = set(["id", "result", "error"]) + POSSIBLE_FIELDS = set(["id", "result", "error"]) @property def data(self): @@ -134,11 +136,14 @@ def error(self): @error.setter def error(self, value): - self._data.pop('value', None) if value: - self._data["error"] = value + if self.result is not None: + raise ValueError("Either result or error should be used") # Test error JSONRPCError(**value) + self._data["error"] = value + else: + self._data["error"] = None @property def _id(self): @@ -149,3 +154,26 @@ def _id(self, value): if value is None: raise ValueError("id could not be null for JSON-RPC1.0 Response") self._data["id"] = value + + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + if not isinstance(data, dict): + raise ValueError("data should be dict") + + if cls.REQUIRED_FIELDS <= set(data.keys()) <= cls.POSSIBLE_FIELDS: + try: + return cls( + _id=data["id"], result=data["result"], error=data["error"] + ) + except ValueError as e: + raise JSONRPCInvalidResponseException(str(e)) + else: + extra = set(data.keys()) - cls.POSSIBLE_FIELDS + missed = cls.REQUIRED_FIELDS - set(data.keys()) + msg = "Invalid response. Extra fields: {0}, Missed fields: {1}" + raise JSONRPCInvalidResponseException(msg.format(extra, missed)) diff --git a/jsonrpc/jsonrpc2.py b/jsonrpc/jsonrpc2.py index 66ca451..7b34f3f 100644 --- a/jsonrpc/jsonrpc2.py +++ b/jsonrpc/jsonrpc2.py @@ -1,7 +1,7 @@ from . import six import json -from .exceptions import JSONRPCError, JSONRPCInvalidRequestException +from .exceptions import JSONRPCError, JSONRPCInvalidRequestException, JSONRPCInvalidResponseException from .base import JSONRPCBaseRequest, JSONRPCBaseResponse @@ -199,6 +199,8 @@ class JSONRPC20Response(JSONRPCBaseResponse): """ JSONRPC_VERSION = "2.0" + REQUIRED_FIELDS = set(["jsonrpc", "id"]) + POSSIBLE_FIELDS = set(["jsonrpc", "id", "result", "error"]) @property def data(self): @@ -222,17 +224,35 @@ def result(self, value): raise ValueError("Either result or error should be used") self._data["result"] = value + @result.deleter + def result(self): + try: + del self._data["result"] + except KeyError: + pass + @property def error(self): return self._data.get("error") @error.setter def error(self, value): - self._data.pop('value', None) if value: - self._data["error"] = value + if self.result is not None: + raise ValueError("Either result or error should be used") + del self.result # Test error JSONRPCError(**value) + self._data["error"] = value + else: + del self.error + + @error.deleter + def error(self): + try: + del self._data["error"] + except KeyError: + pass @property def _id(self): @@ -246,6 +266,47 @@ def _id(self, value): self._data["id"] = value + @classmethod + def from_json(cls, json_str): + data = cls.deserialize(json_str) + return cls.from_data(data) + + @classmethod + def from_data(cls, data): + is_batch = isinstance(data, list) + data = data if is_batch else [data] + + if not data: + raise JSONRPCInvalidResponseException("[] value is not accepted") + + if not all(isinstance(d, dict) for d in data): + raise JSONRPCInvalidResponseException( + "Each response should be an object (dict)") + + result = [] + for d in data: + if not cls.REQUIRED_FIELDS <= set(d.keys()) <= cls.POSSIBLE_FIELDS: + extra = set(d.keys()) - cls.POSSIBLE_FIELDS + missed = cls.REQUIRED_FIELDS - set(d.keys()) + msg = "Invalid response. Extra fields: {0}, Missed fields: {1}" + raise JSONRPCInvalidResponseException(msg.format(extra, missed)) + s = set(['result', 'error']) & set(d.keys()) + if len(s) != 1: + if len(s) == 2: + msg = "Invalid response. Either result or error may be present, not both." + else: + msg = "Invalid response. Neither result nor error present." + raise JSONRPCInvalidResponseException(msg) + + try: + result.append(JSONRPC20Response( + _id=d.get("id"), result=d.get("result"), error=d.get("error") + )) + except ValueError as e: + raise JSONRPCInvalidResponseException(str(e)) + + return JSONRPC20BatchResponse(*result) if is_batch else result[0] + class JSONRPC20BatchResponse(object): diff --git a/jsonrpc/tests/test_jsonrpc1.py b/jsonrpc/tests/test_jsonrpc1.py index c66f045..c0cfa81 100644 --- a/jsonrpc/tests/test_jsonrpc1.py +++ b/jsonrpc/tests/test_jsonrpc1.py @@ -1,7 +1,7 @@ import json import sys -from ..exceptions import JSONRPCInvalidRequestException +from ..exceptions import JSONRPCInvalidRequestException, JSONRPCInvalidResponseException from ..jsonrpc1 import ( JSONRPC10Request, JSONRPC10Response, @@ -427,3 +427,85 @@ def test_data_setter(self): def test_validation_id(self): response = JSONRPC10Response(**self.response_success_params) self.assertEqual(response._id, self.response_success_params["_id"]) + + def test_from_json_invalid_response_result(self): + str_json = json.dumps({ + "error": {"code": -32700, "message": "Parse error"}, + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC10Response.from_json(str_json) + + def test_from_json_invalid_response_error(self): + str_json = json.dumps({ + "result": "", + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC10Response.from_json(str_json) + + def test_from_json_invalid_response_id(self): + str_json = json.dumps({ + "result": "", + "error": None, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC10Response.from_json(str_json) + + def test_from_json_invalid_response_both_result_and_error(self): + str_json = json.dumps({ + "result": "", + "error": {"code": -32700, "message": "Parse error"}, + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC10Response.from_json(str_json) + + def test_from_json_invalid_response_extra_data(self): + str_json = json.dumps({ + "result": "", + "error": None, + "id": 0, + "is_notification": True, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC10Response.from_json(str_json) + + def test_from_json_response_result(self): + str_json = json.dumps({ + "result": "abc", + "error": None, + "id": 0, + }) + + response = JSONRPC10Response.from_json(str_json) + self.assertTrue(isinstance(response, JSONRPC10Response)) + self.assertEqual(response.result, "abc") + self.assertIsNone(response.error) + self.assertEqual(response._id, 0) + + def test_from_json_response_error(self): + error = {'code': 1, 'message': ''} + str_json = json.dumps({ + "result": None, + "error": error, + "id": 0, + }) + + response = JSONRPC10Response.from_json(str_json) + self.assertTrue(isinstance(response, JSONRPC10Response)) + self.assertIsNone(response.result) + self.assertEqual(response.error, error) + self.assertEqual(response._id, 0) + + def test_from_json_string_not_dict(self): + with self.assertRaises(ValueError): + JSONRPC10Response.from_json("[]") + + with self.assertRaises(ValueError): + JSONRPC10Response.from_json("0") diff --git a/jsonrpc/tests/test_jsonrpc2.py b/jsonrpc/tests/test_jsonrpc2.py index 32c1639..8c9e946 100644 --- a/jsonrpc/tests/test_jsonrpc2.py +++ b/jsonrpc/tests/test_jsonrpc2.py @@ -1,7 +1,7 @@ import json import sys -from ..exceptions import JSONRPCInvalidRequestException +from ..exceptions import JSONRPCInvalidRequestException, JSONRPCInvalidResponseException from ..jsonrpc2 import ( JSONRPC20Request, JSONRPC20BatchRequest, @@ -691,6 +691,96 @@ def test_data_setter(self): with self.assertRaises(ValueError): response.data = None + def test_from_json_invalid_response_jsonrpc(self): + str_json = json.dumps({ + "result": None, + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC20Response.from_json(str_json) + + def test_from_json_invalid_response_id(self): + str_json = json.dumps({ + "result": None, + "jsonrpc": "2.0", + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC20Response.from_json(str_json) + + def test_from_json_invalid_response_no_result_error(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC20Response.from_json(str_json) + + def test_from_json_invalid_response_result_and_error(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + "result": None, + "error": {"code": 1, "message": ""} + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC20Response.from_json(str_json) + + def test_from_json_invalid_response_extra_data(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + "result": None, + "error": {"code": 1, "message": ""}, + "extra-data": "" + }) + + with self.assertRaises(JSONRPCInvalidResponseException): + JSONRPC20Response.from_json(str_json) + + def test_from_json_response_result_null(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + "result": None, + }) + + response = JSONRPC20Response.from_json(str_json) + self.assertIsInstance(response, JSONRPC20Response) + self.assertIsNone(response.result) + self.assertIsNone(response.error) + self.assertEqual(response._id, 0) + + def test_from_json_response_result(self): + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + "result": [1, 2, 3], + }) + + response = JSONRPC20Response.from_json(str_json) + self.assertIsInstance(response, JSONRPC20Response) + self.assertEqual(response.result, [1, 2, 3]) + self.assertIsNone(response.error) + self.assertEqual(response._id, 0) + + def test_from_json_response_error(self): + error = {'code': 1, 'message': ''} + str_json = json.dumps({ + "jsonrpc": "2.0", + "id": 0, + "error": error, + }) + + response = JSONRPC20Response.from_json(str_json) + self.assertIsInstance(response, JSONRPC20Response) + self.assertIsNone(response.result) + self.assertEqual(response.error, error) + self.assertEqual(response._id, 0) + class TestJSONRPC20BatchResponse(unittest.TestCase):