IPv6: don't check access to metadata for IPv6 VM
[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 normalize_json_text(text, strict=False, indent=1, keys_with_bits=[], jmes_path=None):
158     """
159     Attempt to return sorted indented JSON string.
160
161     If jmes_path is set the related subset of JSON data is returned as
162     indented JSON string if the subset exists. Empty string is returned if the
163     subset doesn't exist.
164     Empty string is returned if text is not passed.
165     If parse error happens:
166     If strict is true, raise the exception.
167     If strict is not true, return original text with error message.
168     If keys_with_bits is non-empty, run sort_bits on intermediate Python object.
169     """
170
171     if not text:
172         return ""
173
174     if jmes_path:
175         json_obj = _json.loads(text)
176         subset = jmespath.search(jmes_path, json_obj)
177         if not subset:
178             return ""
179         text = _json.dumps(subset)
180
181     try:
182         object_decoded = loads_sorted(text)
183     except ValueError as err:
184         if strict:
185             raise err
186         else:
187             return str(err) + '\n' + text
188     if keys_with_bits:
189         sort_bits(object_decoded, keys_with_bits)
190
191     pretty_json = dumps_indented(object_decoded, indent=indent)
192
193     return pretty_json