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