Package tlib :: Package base :: Module JsonHelper
[hide private]
[frames] | no frames]

Source Code for Module tlib.base.JsonHelper

  1  import re 
  2  import difflib 
  3  # noinspection PyPackageRequirements 
  4  from datadiff.tools import assert_equals 
  5  import json 
  6  # noinspection PyPackageRequirements 
  7  import pytest 
  8  from collections import OrderedDict 
  9  from warnings import warn 
 10  from tlib.base.TestHelper import sort_list, sort_dict 
 11  import jsonpath_rw 
 12  from tlib.base.ExceptionHelper import BadJsonFormatError 
13 14 # noinspection PyUnresolvedReferences 15 -class JsonHelper(object):
16 """ 17 Functions for modifying and validating JSON data 18 """ 19 20 @staticmethod
21 - def assert_json_equals(json1, json2, ignore_order=True, exclude_keys=list()):
22 """ 23 Compares two JSON values\n 24 If parameter validate_order is True, position of keys is taken into account during validation\n 25 If json1 or json2 are strings, it must be a valid JSON string 26 27 @param json1: String or dictionary to compare 28 @type json1: str, dict 29 @param json2: String or dictionary to compare 30 @type json2: str, dict 31 @param ignore_order: If true, order of keys is not validated 32 @type ignore_order: bool 33 @param exclude_keys: Keys to exclude from comparison, in the format 'key2.key3[2].key1' 34 @type exclude_keys: list 35 @raise AssertionError: JSON values doesn't match 36 """ 37 if ignore_order: 38 JsonHelper._validate_json_ignoring_order(json1, json2, exclude_keys) 39 else: 40 JsonHelper._validate_json_with_order(json1, json2, exclude_keys)
41 42 @staticmethod
43 - def _validate_json_ignoring_order(json1, json2, exclude_keys):
44 """ 45 Compares two JSON values, ignoring position of keys\n 46 If json1 or json2 are strings, it must be a valid JSON string 47 @param json1: String or dictionary to compare 48 @type json1: str, dict 49 @param json2: String or dictionary to compare 50 @type json2: str, dict 51 @raise AssertionError: JSON values doesn't match 52 """ 53 items = [json1, json2] 54 for i, item in enumerate(items): 55 #If it's a string, convert to dictionary 56 if isinstance(item, basestring): 57 try: 58 item = json.loads(item) 59 except ValueError: 60 pytest.fail("Value doesn't represent a valid JSON string\n%s" % item) 61 62 #make sure any lists inside the JSON value are sorted, so diff will not fail 63 if type(item) is dict: 64 #Sort items inside lists 65 item = sort_dict(item) 66 elif type(item) is list: 67 #Sort items inside lists 68 item = sort_list(item) 69 else: 70 pytest.fail("Parameter doesn't represent a valid JSON object: %s" % item) 71 72 #Delete unwanted keys 73 JsonHelper.remove_keys(item, exclude_keys) 74 75 items[i] = item 76 77 assert_equals(items[0], items[1])
78 79 @staticmethod
80 - def _validate_json_with_order(json1, json2, exclude_keys=list()):
81 """ 82 Compares two JSON values, taking into account key order 83 If json1 or json2 are strings, it must be a valid JSON string 84 @param json1: String to compare 85 @type json1: str 86 @param json2: String to compare 87 @type json2: str 88 @param exclude_keys: Keys to exclude from comparison, in the format 'key2.key3[2].key1' 89 @type exclude_keys: list 90 @raise AssertionError: JSON values doesn't match 91 """ 92 93 def object_pairs_hook(values): 94 """ 95 Method that stores objects in an OrderedDict so comparison takes into account key order 96 """ 97 return OrderedDict(values)
98 99 items = [json1, json2] 100 for i, item in enumerate(items): 101 if not isinstance(item, basestring): 102 pytest.fail("Only strings are allowed when validating key order") 103 104 # By default json.loads doesn't care about key order. Let's provide 105 # an object_pairs_hook function to ensure key order is kept 106 try: 107 item = json.loads(item, object_pairs_hook=object_pairs_hook) 108 except ValueError: 109 pytest.fail("Value doesn't represent a valid JSON string\n%s" % item) 110 111 #Delete unwanted keys 112 JsonHelper.remove_keys(item, exclude_keys) 113 114 items[i] = item 115 116 #Do validation 117 if items[0] != items[1]: 118 json1 = json.dumps(items[0], indent=3).splitlines(True) 119 json2 = json.dumps(items[1], indent=3).splitlines(True) 120 diff = difflib.unified_diff(json1, json2) 121 diff_txt = "".join(diff) 122 123 raise AssertionError(r"JSON values didn't match\nValue1:\n%s\n\nValue2:\n%s\n\nDiff:\n%s""" % 124 ("".join(json1), "".join(json2), diff_txt))
125 126 @staticmethod
127 - def remove_keys(data, keys, top_level=True):
128 """ 129 Takes a dictionary or a list and removes keys specified by 'keys' parameter 130 @param data: dictionary or OrderedDict that will be modified. 131 Object is replaced in place 132 @type data: dictionary, OrderedDict 133 @param keys: array indicating the path to remove. 134 e.g. ['businessHours.headings[2]', 'businessHours.values.name'] 135 @type keys: list 136 """ 137 for key in keys: 138 JsonHelper._remove_key(data, key.split('.'), top_level)
139 140 @staticmethod
141 - def _remove_key(data, key, top_level=True):
142 """ 143 Takes a dictionary or a list and removes keys specified by 'keys' parameter 144 @param data: dictionary or OrderedDict that will be modified. 145 Object is replaced in place 146 @type data: dictionary, OrderedDict 147 @param key: array indicating the path to remove. e.g. ['businessHours', 'headings[2]'] 148 @type key: list 149 """ 150 orig_key = None 151 152 #Parameter validation 153 if type(key) is not list: 154 pytest.fail("Invalid argument key. Was expecting a list") 155 156 if not (type(data) in (dict, list) or isinstance(data, OrderedDict)): 157 pytest.fail("Invalid argument data. Was expecting a dict or OrderedDict") 158 159 if top_level: 160 #Create a copy of the key object so we don't modify the original 161 orig_key = list(key) 162 163 #Check if first element in the keys has syntax '*' and change it to be able to match the right values 164 match = re.search("^\*\[(.*)\]$", key[0]) 165 if match: 166 try: 167 key[0] = int(match.group(1)) 168 except ValueError: 169 pytest.fail("Index '%s' is not a valid integer" % match.group(2)) 170 171 # split indexed items in two, only first time function is called 172 # eg. ["node1", "node[2]"] => ["node1", "node", 2] 173 new_key = list() 174 175 for i, value in enumerate(key): 176 match = re.search("(^.+)\[(.+)\]$", str(value)) 177 if match: 178 try: 179 new_key.append(match.group(1)) 180 new_key.append(int(match.group(2))) 181 except ValueError: 182 pytest.fail("Index '%s' is not a valid integer" % match.group(2)) 183 184 else: 185 new_key.append(value) 186 187 key = list(new_key) 188 189 if type(data) is list: 190 #check if next key is an index, otherwise fail 191 if type(key[0]) is int: 192 index = key.pop(0) 193 194 if len(key) == 0: 195 #Found the key 196 data.pop(index) 197 else: 198 #Still need to find children nodes 199 JsonHelper._remove_key(data[index], key, top_level=False) 200 201 else: 202 pytest.fail("Key '%s' is not valid for the given JSON object" % key) 203 elif type(data) is dict or isinstance(data, OrderedDict): 204 #Validate 205 for k in data: 206 if k == key[0]: 207 key.pop(0) 208 209 if len(key) == 0: 210 #Found the key 211 del(data[k]) 212 else: 213 #Still need to find children nodes 214 JsonHelper._remove_key(data[k], key, top_level=False) 215 216 # Don't need to continue iterating. Node was already found 217 break 218 else: 219 pytest.fail("Element %s is not allowed in a JSON response" % type(data)) 220 221 if len(key) > 0: 222 # Not all keys were found. Can't fail here because it would make impossible testing the scenario where 223 # a JSON payload has a key and other doesn't 224 warn("Key '%s' does not represent a valid element" % '.'.join(orig_key)) 225 226 if top_level: 227 #Restore key variable 228 # noinspection PyUnusedLocal 229 key = list(orig_key)
230 231 @staticmethod
232 - def get_elements(data, json_path):
233 """" 234 Applies a JSON path to a JSON string and returns the resulting node 235 @param data: 236 @type data: str, dict 237 """ 238 #If it's a string, convert to dictionary 239 if isinstance(data, basestring): 240 try: 241 json_data = json.loads(data) 242 except ValueError: 243 pytest.fail("Value doesn't represent a valid JSON string\n%s" % item) 244 elif type(data) is dict: 245 #Sort items inside lists 246 json_data = data 247 else: 248 raise BadJsonFormatError("Parameter doesn't represent a valid JSON object: %s" % item) 249 250 jsonpath_expr = jsonpath_rw.parse(json_path) 251 result = [] 252 for i in jsonpath_expr.find(json_data): 253 result.append(i.value) 254 255 return result
256