c323c742a99ea9ee6d8172499c919c50eeacc82a
[integration/test.git] / csit / libraries / norm_json.py
1 """This module contains single a function for normalizing JSON strings."""
2 # Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3 #
4 # This program and the accompanying materials are made available under the
5 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 # and is available at http://www.eclipse.org/legal/epl-v10.html
7
8 import collections as _collections
9 import jmespath
10 try:
11     import simplejson as _json
12 except ImportError:  # Python2.7 calls it json.
13     import json as _json
14
15
16 __author__ = "Vratko Polak"
17 __copyright__ = "Copyright(c) 2015-2016, Cisco Systems, Inc."
18 __license__ = "Eclipse Public License v1.0"
19 __email__ = "vrpolak@cisco.com"
20
21
22 # Internal details; look down below for Robot Keywords.
23
24
25 class _Hsfl(list):
26     """
27     Hashable sorted frozen list implementation stub.
28
29     Supports only __init__, __repr__ and __hash__ methods.
30     Other list methods are available, but they may break contract.
31     """
32
33     def __init__(self, *args, **kwargs):
34         """Contruct super, sort and compute repr and hash cache values."""
35         sup = super(_Hsfl, self)
36         sup.__init__(*args, **kwargs)
37         sup.sort(key=repr)
38         self.__repr = repr(tuple(self))
39         self.__hash = hash(self.__repr)
40
41     def __repr__(self):
42         """Return cached repr string."""
43         return self.__repr
44
45     def __hash__(self):
46         """Return cached hash."""
47         return self.__hash
48
49
50 class _Hsfod(_collections.OrderedDict):
51     """
52     Hashable sorted (by key) frozen OrderedDict implementation stub.
53
54     Supports only __init__, __repr__ and __hash__ methods.
55     Other OrderedDict methods are available, but they may break contract.
56     """
57
58     def __init__(self, *args, **kwargs):
59         """Put arguments to OrderedDict, sort, pass to super, cache values."""
60         self_unsorted = _collections.OrderedDict(*args, **kwargs)
61         items_sorted = sorted(self_unsorted.items(), key=repr)
62         sup = super(_Hsfod, self)  # possibly something else than OrderedDict
63         sup.__init__(items_sorted)
64         # Repr string is used for sorting, keys are more important than values.
65         self.__repr = '{' + repr(self.keys()) + ':' + repr(self.values()) + '}'
66         self.__hash = hash(self.__repr)
67
68     def __repr__(self):
69         """Return cached repr string."""
70         return self.__repr
71
72     def __hash__(self):
73         """Return cached hash."""
74         return self.__hash
75
76
77 def _hsfl_array(s_and_end, scan_once, **kwargs):
78     """Scan JSON array as usual, but return hsfl instead of list."""
79     values, end = _json.decoder.JSONArray(s_and_end, scan_once, **kwargs)
80     return _Hsfl(values), end
81
82
83 class _Decoder(_json.JSONDecoder):
84     """Private class to act as customized JSON decoder.
85
86     Based on: http://stackoverflow.com/questions/10885238/
87     python-change-list-type-for-json-decoding"""
88
89     def __init__(self, **kwargs):
90         """Initialize decoder with special array implementation."""
91         _json.JSONDecoder.__init__(self, **kwargs)
92         # Use the custom JSONArray
93         self.parse_array = _hsfl_array
94         # Use the python implemenation of the scanner
95         self.scan_once = _json.scanner.py_make_scanner(self)
96
97
98 # Robot Keywords; look above for internal details.
99
100
101 def loads_sorted(text, strict=False):
102     """Return Python object with sorted arrays and dictionary keys."""
103     object_decoded = _json.loads(text, cls=_Decoder, object_hook=_Hsfod)
104     return object_decoded
105
106
107 def dumps_indented(obj, indent=1):
108     """
109     Wrapper for json.dumps with default indentation level.
110
111     The main value is that BuiltIn.Evaluate cannot easily accept Python object
112     as part of its argument.
113     Also, allows to use something different from RequestsLibrary.To_Json
114
115     """
116     pretty_json = _json.dumps(obj, separators=(',', ': '), indent=indent)
117     return pretty_json + '\n'  # to avoid diff "no newline" warning line
118
119
120 def sort_bits(obj, keys_with_bits=[]):
121     """
122     Rearrange string values of list bits names in alphabetical order.
123
124     This function looks at dict items with known keys.
125     If the value is string, space-separated names are sorted.
126     This function is recursive over dicts and lists.
127     Current implementation performs re-arranging in-place (to save memory),
128     so it is not required to store the return value.
129
130     The intended usage is for composite objects which contain
131     OrderedDict elements. The implementation makes sure that ordering
132     (dictated by keys) is preserved. Support for generic dicts is an added value.
133
134     Sadly, dict (at least in Python 2.7) does not have __updatevalue__(key) method
135     which would guarantee iteritems() is not affected when value is updated.
136     Current "obj[key] = value" implementation is safe for dict and OrderedDict,
137     but it may be not safe for other subclasses of dict.
138
139     TODO: Should this docstring include links to support dict and OrderedDict safety?
140     """
141     if isinstance(obj, dict):
142         for key, value in obj.iteritems():
143             # Unicode is not str and vice versa, isinstance has to check for both.
144             # Luckily, "in" recognizes equivalent strings in different encodings.
145             # Type "bytes" is added for Python 3 compatibility.
146             if key in keys_with_bits and isinstance(value, (unicode, str, bytes)):
147                 obj[key] = " ".join(sorted(value.split(" ")))
148             else:
149                 sort_bits(value, keys_with_bits)
150     # A string is not a list, so there is no risk of recursion over characters.
151     elif isinstance(obj, list):
152         for item in obj:
153             sort_bits(item, keys_with_bits)
154     return obj
155
156
157 def hide_volatile(obj, keys_with_volatiles=[]):
158     """
159     Takes list of keys with volatile values, and replaces them with generic "*"
160
161     :param obj: python dict from json
162     :param keys_with_volatiles: list of volatile keys
163     :return: corrected
164     """
165     if isinstance(obj, dict):
166         for key, value in obj.iteritems():
167             # Unicode is not str and vice versa, isinstance has to check for both.
168             # Luckily, "in" recognizes equivalent strings in different encodings.
169             # Type "bytes" is added for Python 3 compatibility.
170             if key in keys_with_volatiles and isinstance(value, (unicode, str, bytes, int)):
171                 obj[key] = "*"
172             else:
173                 hide_volatile(value, keys_with_volatiles)
174     # A string is not a list, so there is no risk of recursion over characters.
175     elif isinstance(obj, list):
176         for item in obj:
177             hide_volatile(item, keys_with_volatiles)
178     return obj
179
180
181 def normalize_json_text(text, strict=False, indent=1, keys_with_bits=[], keys_with_volatiles=[], jmes_path=None):
182     """
183     Attempt to return sorted indented JSON string.
184
185     If jmes_path is set the related subset of JSON data is returned as
186     indented JSON string if the subset exists. Empty string is returned if the
187     subset doesn't exist.
188     Empty string is returned if text is not passed.
189     If parse error happens:
190     If strict is true, raise the exception.
191     If strict is not true, return original text with error message.
192     If keys_with_bits is non-empty, run sort_bits on intermediate Python object.
193     """
194
195     if not text:
196         return ""
197
198     if jmes_path:
199         json_obj = _json.loads(text)
200         subset = jmespath.search(jmes_path, json_obj)
201         if not subset:
202             return ""
203         text = _json.dumps(subset)
204
205     try:
206         object_decoded = loads_sorted(text)
207     except ValueError as err:
208         if strict:
209             raise err
210         else:
211             return str(err) + '\n' + text
212     if keys_with_bits:
213         sort_bits(object_decoded, keys_with_bits)
214     if keys_with_volatiles:
215         hide_volatile(object_decoded, keys_with_volatiles)
216
217     pretty_json = dumps_indented(object_decoded, indent=indent)
218
219     return pretty_json