diff --git a/changelogs/fragments/client_object_hook.yml b/changelogs/fragments/client_object_hook.yml new file mode 100644 index 00000000..b70abe05 --- /dev/null +++ b/changelogs/fragments/client_object_hook.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - client - allow user to pass a `object_hook` function to rest client for custom decoding of the json response(https://github.com/ansible-collections/servicenow.itsm/pull/316). diff --git a/plugins/module_utils/client.py b/plugins/module_utils/client.py index b856661b..9a4402c9 100644 --- a/plugins/module_utils/client.py +++ b/plugins/module_utils/client.py @@ -20,7 +20,7 @@ class Response: - def __init__(self, status, data, headers=None): + def __init__(self, status, data, headers=None, json_decoder_hook=None): self.status = status self.data = data # [('h1', 'v1'), ('H2', 'V2')] -> {'h1': 'v1', 'h2': 'V2'} @@ -29,12 +29,13 @@ def __init__(self, status, data, headers=None): ) self._json = None + self.json_decoder_hook = json_decoder_hook @property def json(self): if self._json is None: try: - self._json = json.loads(self.data) + self._json = json.loads(self.data, object_hook=self.json_decoder_hook) except ValueError: raise ServiceNowError( "Received invalid JSON response: {0}".format(self.data) @@ -57,6 +58,7 @@ def __init__( api_path="api/now", timeout=None, validate_certs=None, + json_decoder_hook=None, ): if not (host or "").startswith(("https://", "http://")): raise ServiceNowError( @@ -77,6 +79,7 @@ def __init__( self.access_token = access_token self.timeout = timeout self.validate_certs = validate_certs + self.json_decoder_hook = json_decoder_hook self._auth_header = None self._client = Request() @@ -158,8 +161,15 @@ def _request(self, method, path, data=None, headers=None): raise ServiceNowError(e.reason) if PY2: - return Response(raw_resp.getcode(), raw_resp.read(), raw_resp.info()) - return Response(raw_resp.status, raw_resp.read(), raw_resp.headers) + return Response( + raw_resp.getcode(), + raw_resp.read(), + raw_resp.info(), + self.json_decoder_hook, + ) + return Response( + raw_resp.status, raw_resp.read(), raw_resp.headers, self.json_decoder_hook + ) def request(self, method, path, query=None, data=None, headers=None, bytes=None): # Make sure we only have one kind of payload diff --git a/tests/unit/plugins/module_utils/test_client.py b/tests/unit/plugins/module_utils/test_client.py index 64ca418b..cabbc28c 100644 --- a/tests/unit/plugins/module_utils/test_client.py +++ b/tests/unit/plugins/module_utils/test_client.py @@ -21,6 +21,18 @@ ) +def custom_decoder_hook(dct): + if "result" in dct: + if not isinstance(dct["result"], list): + return dct + + item = dct["result"] + for k in item: + if "__meta" in k: + item.remove(k) + return dct + + class TestResponseInit: @pytest.mark.parametrize( "raw_headers,expected_headers", @@ -67,6 +79,24 @@ def test_json_is_cached(self, mocker): assert json_mock.loads.call_count == 1 + def test_custom_decoder_hook(self): + resp = client.Response( + 200, + '{"result": [{"__meta": {"encodedQuery": "some_query"}}]}', + headers=[("Content-type", "application/json")], + json_decoder_hook=custom_decoder_hook, + ) + assert resp.json == {"result": []} + + def test_custom_decoder_hook_with_values(self): + resp = client.Response( + 200, + '{"result": [{"some_obj": "value"},{"__meta": {"encodedQuery": "some_query"}}]}', + headers=[("Content-type", "application/json")], + json_decoder_hook=custom_decoder_hook, + ) + assert resp.json == {"result": [{"some_obj": "value"}]} + class TestClientInit: @pytest.mark.parametrize("host", [None, "", "invalid", "missing.schema"])