Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON RPC response parsing #99

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ Laurent Mazuel (@lmazuel) <lmazuel@microsoft.com>
Igor Melnyk (@liminspace) <igormeln@gmail.com>
Ghislain Antony Vaillant (@ghisvail) <ghisvail@gmail.com>
Chris Jerdonek (@cjerdonek) <chris.jerdonek@gmail.com>
Kacper Kubkowski (@kacper-ka) <kkubkowski@gmail.com>
7 changes: 7 additions & 0 deletions jsonrpc/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ class JSONRPCInvalidRequestException(JSONRPCException):
pass


class JSONRPCInvalidResponseException(JSONRPCException):

""" Response is not valid."""

pass


class JSONRPCDispatchException(JSONRPCException):

""" JSON-RPC Dispatch Exception.
Expand Down
17 changes: 17 additions & 0 deletions jsonrpc/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
34 changes: 31 additions & 3 deletions jsonrpc/jsonrpc1.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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))
67 changes: 64 additions & 3 deletions jsonrpc/jsonrpc2.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):

Expand Down
84 changes: 83 additions & 1 deletion jsonrpc/tests/test_jsonrpc1.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
import sys

from ..exceptions import JSONRPCInvalidRequestException
from ..exceptions import JSONRPCInvalidRequestException, JSONRPCInvalidResponseException
from ..jsonrpc1 import (
JSONRPC10Request,
JSONRPC10Response,
Expand Down Expand Up @@ -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")
Loading