1

I am using Python to test a REST API. I get a response message as json and want to check each field. I have created a check_json function and check against a checker dictionary. The checker dictionary has a string key which is the name of the key in json with value a tuple (a pair) with first bool parameter, whether item is mandatory and second parameter either an object to do direct compare or a function to add more involved checking functionality.

I make the check like this:

r= check_json(myjson, checker)

where r is the result in format: 'field': True or False - dependent on whether check passed or failed

The code is a little messay with lots of global functions. One idea was to include the is checking functions in check_json. I have been told that I could also use closures. But how?

Here is my code:

# check nested json
import json
import collections
import functools
import datetime

#this is json from GET session
myjson = {
    "accessed": "Wed, 31 Jul 2013 13:03:38 GMT",
    "created": "Wed, 31 Jul 2013 13:03:38 GMT",
    "dnUrls": [
        "http://135.86.180.69:8580/ucc/api/v1/session/dns/50000"
    ],
    "expires": "Wed, 31 Jul 2013 13:03:48 GMT",
    "modified": "Wed, 31 Jul 2013 13:03:38 GMT",
    "name": "KW50000",
    "person": {
        "employeeId": "KW50000",
        "firstName": "KW50000",
        "lastName": "Dev5"
    },
    "previewRedirect": {
        "destination": "",
        "enabled": False,
        "setupEnabled": False
    }
}

def isDate(s):
    try:
        testdate = datetime.datetime.strptime(s, '%a, %d %b %Y %H:%M:%S GMT')
        return True
    except Exception:
        print "conversion to date failed"
        return False

def isURL(l):
    count = 0
    for item in l:
        count += 1 if item.startswith("http://") else (-1)

    return count > 0

def isAlnum(s):
    return s.isalnum()

def isBool(s):
    return type(s) == bool

def isAny(s):
    return True


#checker object made up of dictionary with string key and tuple value (a pair)
#tuple first filed is flag indicating whether key is mandatory or not.
# tuple 2nd key is value to expect or a checker function
checker = {
    "accessed": (True, isDate),
    "created": (True, isDate),
    "dnUrls": (True, isURL),
    "expires": (True, isDate),
    "modified": (True, isDate),
    "name": (True, "KW50000"),
    "person": (True, {
        "employeeId": (True, isAlnum),
        "firstName": (False, isAny),
        "lastName": (False, isAny)
    }),
    "previewRedirect": (True, {
        "destination": (False, isAny),
        "enabled": (True, isBool),
        "setupEnabled": (False, isBool)
    })
}


# returns dictionary with key= fieldname, value = result, either True (Test Pass), False (test Failed)
def check_json(obj, checker):
    """params json to check, template comparison object
       returns dictionary of keys to pass/fail values"""

    result = {}
    for k, (mFlag, chk) in checker.iteritems():
        if not k in obj:
            result[k] = not mFlag
        elif isinstance(chk, collections.Callable):    
            result[k] = chk(obj[k])
        elif(isinstance(chk, collections.Mapping)):
            result[k] = check_json(obj[k], chk)
        else:       
            result[k] = chk == obj[k]
    return result

def with_and(v1, v2):
    return functools.reduce(with_and, v2.itervalues(), v1) if isinstance(v2, collections.Mapping) else v1 and v2


r= check_json(myjson, checker)
print "r={}".format(r)
isOK = functools.reduce(with_and, r.itervalues(), True)
print "Result is {}: {}".format(isOK, r)
Angus Comber
  • 9,316
  • 14
  • 59
  • 107
  • You might be able to do something like create the checker template inside a function and [alter its namespace](http://stackoverflow.com/q/1142068/646543) before calling or, or hijack the "with" statement somehow to provide those functions and create the template function inside it. This approach might just end up being more confusing and more work then it's worth though. – Michael0x2a Jul 31 '13 at 16:05

1 Answers1

0

Here is a possible approach. You will have to decide whether it is more readable/maintainable/etc.

# check nested json
import collections
from functools import reduce
import datetime

#this is json from GET session
myjson = {
    "accessed": "Wed, 31 Jul 2013 13:03:38 GMT",
    "created": "Wed, 31 Jul 2013 13:03:38 GMT",
    "dnUrls": [
        "http://135.86.180.69:8580/ucc/api/v1/session/dns/50000"
    ],
    "expires": "Wed, 31 Jul 2013 13:03:48 GMT",
    "modified": "Wed, 31 Jul 2013 13:03:38 GMT",
    "name": "KW50000",
    "person": {
        "employeeId": "KW50000",
        "firstName": "KW50000",
        "lastName": "Dev5"
    },
    "previewRedirect": {
        "destination": "",
        "enabled": False,
        "setupEnabled": False
    }
}


# returns dictionary with key= fieldname, value = result,
# either True (Test Pass), False (test Failed)
def check_json(obj, checker):
    """params json to check, template comparison object
       returns dictionary of keys to pass/fail values"""

    result = {}
    for k, (mFlag, chk) in checker.items():
        if not k in obj:
            result[k] = not mFlag
        elif isinstance(chk, collections.Callable):
            result[k] = chk(obj[k])
        elif(isinstance(chk, collections.Mapping)):
            result[k] = check_json(obj[k], chk)
        else:
            result[k] = chk == obj[k]
    return result

    def isDate(s):
        try:
            datetime.datetime.strptime(s, '%a, %d %b %Y %H:%M:%S GMT')
            return True
        except Exception:
            print("conversion to date failed")
            return False


#checker object made up of dictionary with string key and tuple value (a pair)
#tuple first filed is flag indicating whether key is mandatory or not.
# tuple 2nd key is value to expect or a checker function
checker = {
    "accessed": (True, check_json.isDate),
    "created": (True, check_json.isDate),
    "dnUrls": (True, lambda l: (reduce(lambda c, v:\
                     c + (1 if v.startswith('http://') else -1), l, 0) > 0)),
    "expires": (True, check_json.isDate),
    "modified": (True, check_json.isDate),
    "name": (True, "KW50000"),
    "person": (True, {
        "employeeId": (True, lambda s: s.isalnum()),
        "firstName": (False, True),
        "lastName": (False, True)
    }),
    "previewRedirect": (True, {
        "destination": (False, True),
        "enabled": (True, lambda s: type(s) is bool),
        "setupEnabled": (False, lambda s: type(s) is bool)
    })
}


def with_and(v1, v2):
    return functools.reduce(with_and, v2.values(), v1)\
                    if isinstance(v2, collections.Mapping) else v1 and v2


r = check_json(myjson, checker)
print("r={}".format(r))
isOK = functools.reduce(with_and, r.values(), True)
print("Result is {}: {}".format(isOK, r))

Note that the code has been modified for Python3. The main change is to use lambdas wherever possible, and to use nested functions otherwise to eliminate global name-space dependencies.

Jonathan
  • 2,635
  • 3
  • 30
  • 49
  • If I run using python 2.7 then I am getting eg "accessed": (True, check_json.isDate), AttributeError: 'function' object has no attribute 'isDate' – Angus Comber Aug 01 '13 at 10:22
  • In that case do not use nested functions and move the `isDate` function back into global scope. – Jonathan Aug 01 '13 at 18:17