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