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