summaryrefslogtreecommitdiffstats
path: root/lib/ansible/module_utils/common/dict_transformations.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/common/dict_transformations.py')
-rw-r--r--lib/ansible/module_utils/common/dict_transformations.py154
1 files changed, 154 insertions, 0 deletions
diff --git a/lib/ansible/module_utils/common/dict_transformations.py b/lib/ansible/module_utils/common/dict_transformations.py
new file mode 100644
index 0000000..ffd0645
--- /dev/null
+++ b/lib/ansible/module_utils/common/dict_transformations.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import re
+from copy import deepcopy
+
+from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+def camel_dict_to_snake_dict(camel_dict, reversible=False, ignore_list=()):
+ """
+ reversible allows two way conversion of a camelized dict
+ such that snake_dict_to_camel_dict(camel_dict_to_snake_dict(x)) == x
+
+ This is achieved through mapping e.g. HTTPEndpoint to h_t_t_p_endpoint
+ where the default would be simply http_endpoint, which gets turned into
+ HttpEndpoint if recamelized.
+
+ ignore_list is used to avoid converting a sub-tree of a dict. This is
+ particularly important for tags, where keys are case-sensitive. We convert
+ the 'Tags' key but nothing below.
+ """
+
+ def value_is_list(camel_list):
+
+ checked_list = []
+ for item in camel_list:
+ if isinstance(item, dict):
+ checked_list.append(camel_dict_to_snake_dict(item, reversible))
+ elif isinstance(item, list):
+ checked_list.append(value_is_list(item))
+ else:
+ checked_list.append(item)
+
+ return checked_list
+
+ snake_dict = {}
+ for k, v in camel_dict.items():
+ if isinstance(v, dict) and k not in ignore_list:
+ snake_dict[_camel_to_snake(k, reversible=reversible)] = camel_dict_to_snake_dict(v, reversible)
+ elif isinstance(v, list) and k not in ignore_list:
+ snake_dict[_camel_to_snake(k, reversible=reversible)] = value_is_list(v)
+ else:
+ snake_dict[_camel_to_snake(k, reversible=reversible)] = v
+
+ return snake_dict
+
+
+def snake_dict_to_camel_dict(snake_dict, capitalize_first=False):
+ """
+ Perhaps unexpectedly, snake_dict_to_camel_dict returns dromedaryCase
+ rather than true CamelCase. Passing capitalize_first=True returns
+ CamelCase. The default remains False as that was the original implementation
+ """
+
+ def camelize(complex_type, capitalize_first=False):
+ if complex_type is None:
+ return
+ new_type = type(complex_type)()
+ if isinstance(complex_type, dict):
+ for key in complex_type:
+ new_type[_snake_to_camel(key, capitalize_first)] = camelize(complex_type[key], capitalize_first)
+ elif isinstance(complex_type, list):
+ for i in range(len(complex_type)):
+ new_type.append(camelize(complex_type[i], capitalize_first))
+ else:
+ return complex_type
+ return new_type
+
+ return camelize(snake_dict, capitalize_first)
+
+
+def _snake_to_camel(snake, capitalize_first=False):
+ if capitalize_first:
+ return ''.join(x.capitalize() or '_' for x in snake.split('_'))
+ else:
+ return snake.split('_')[0] + ''.join(x.capitalize() or '_' for x in snake.split('_')[1:])
+
+
+def _camel_to_snake(name, reversible=False):
+
+ def prepend_underscore_and_lower(m):
+ return '_' + m.group(0).lower()
+
+ if reversible:
+ upper_pattern = r'[A-Z]'
+ else:
+ # Cope with pluralized abbreviations such as TargetGroupARNs
+ # that would otherwise be rendered target_group_ar_ns
+ upper_pattern = r'[A-Z]{3,}s$'
+
+ s1 = re.sub(upper_pattern, prepend_underscore_and_lower, name)
+ # Handle when there was nothing before the plural_pattern
+ if s1.startswith("_") and not name.startswith("_"):
+ s1 = s1[1:]
+ if reversible:
+ return s1
+
+ # Remainder of solution seems to be https://stackoverflow.com/a/1176023
+ first_cap_pattern = r'(.)([A-Z][a-z]+)'
+ all_cap_pattern = r'([a-z0-9])([A-Z]+)'
+ s2 = re.sub(first_cap_pattern, r'\1_\2', s1)
+ return re.sub(all_cap_pattern, r'\1_\2', s2).lower()
+
+
+def dict_merge(a, b):
+ '''recursively merges dicts. not just simple a['key'] = b['key'], if
+ both a and b have a key whose value is a dict then dict_merge is called
+ on both values and the result stored in the returned dictionary.'''
+ if not isinstance(b, dict):
+ return b
+ result = deepcopy(a)
+ for k, v in b.items():
+ if k in result and isinstance(result[k], dict):
+ result[k] = dict_merge(result[k], v)
+ else:
+ result[k] = deepcopy(v)
+ return result
+
+
+def recursive_diff(dict1, dict2):
+ """Recursively diff two dictionaries
+
+ Raises ``TypeError`` for incorrect argument type.
+
+ :arg dict1: Dictionary to compare against.
+ :arg dict2: Dictionary to compare with ``dict1``.
+ :return: Tuple of dictionaries of differences or ``None`` if there are no differences.
+ """
+
+ if not all((isinstance(item, MutableMapping) for item in (dict1, dict2))):
+ raise TypeError("Unable to diff 'dict1' %s and 'dict2' %s. "
+ "Both must be a dictionary." % (type(dict1), type(dict2)))
+
+ left = dict((k, v) for (k, v) in dict1.items() if k not in dict2)
+ right = dict((k, v) for (k, v) in dict2.items() if k not in dict1)
+ for k in (set(dict1.keys()) & set(dict2.keys())):
+ if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
+ result = recursive_diff(dict1[k], dict2[k])
+ if result:
+ left[k] = result[0]
+ right[k] = result[1]
+ elif dict1[k] != dict2[k]:
+ left[k] = dict1[k]
+ right[k] = dict2[k]
+ if left or right:
+ return left, right
+ return None