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