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