From 7b954f45f95d65cb1eda3d4a84c0c91721ab805c Mon Sep 17 00:00:00 2001
From: "Gareth J. Greenaway" <gareth@wiked.org>
Date: Wed, 18 Sep 2019 17:38:00 -0700
Subject: [PATCH] Porting PR #52458 to 2019.2.1

---
 doc/topics/jinja/index.rst     | 43 ++++++++++++++++++++++++++++++++++
 salt/modules/testinframod.py   | 37 ++++-------------------------
 salt/states/testinframod.py    | 20 +++-------------
 salt/utils/stringutils.py      | 36 ++++++++++++++++++++++++++++
 tests/unit/utils/test_jinja.py | 20 ++++++++++++++++
 5 files changed, 106 insertions(+), 50 deletions(-)

diff --git a/doc/topics/jinja/index.rst b/doc/topics/jinja/index.rst
index d8e12cf84d02..d02d690868ce 100644
--- a/doc/topics/jinja/index.rst
+++ b/doc/topics/jinja/index.rst
@@ -1215,6 +1215,49 @@ Returns:
 .. _`builtin filters`: http://jinja.pocoo.org/docs/templates/#builtin-filters
 .. _`timelib`: https://github.com/pediapress/timelib/
 
+
+.. jinja_ref:: to_snake_case
+
+``to_snake_case``
+-----------------
+
+.. versionadded:: Neon
+
+Converts a string from camelCase (or CamelCase) to snake_case.
+
+.. code-block:: jinja
+
+  Example: {{ camelsWillLoveThis | to_snake_case }}
+
+Returns:
+
+.. code-block:: text
+
+  Example: camels_will_love_this
+
+
+.. jinja_ref:: to_camelcase
+
+``to_camelcase``
+----------------
+
+.. versionadded:: Neon
+
+Converts a string from snake_case to camelCase (or UpperCamelCase if so indicated).
+
+.. code-block:: jinja
+
+  Example 1: {{ snake_case_for_the_win | to_camelcase }}
+
+  Example 2: {{ snake_case_for_the_win | to_camelcase(uppercamel=True) }}
+
+Returns:
+
+.. code-block:: text
+
+  Example 1: snakeCaseForTheWin
+  Example 2: SnakeCaseForTheWin
+
 Networking Filters
 ------------------
 
diff --git a/salt/modules/testinframod.py b/salt/modules/testinframod.py
index da9121787312..6620cc52e41e 100644
--- a/salt/modules/testinframod.py
+++ b/salt/modules/testinframod.py
@@ -14,6 +14,8 @@
 import re
 import types
 
+from salt.utils.stringutils import camel_to_snake_case, snake_to_camel_case
+
 log = logging.getLogger(__name__)
 
 try:
@@ -51,38 +53,7 @@ def _get_module(module_name, backend=default_backend):
 
     """
     backend_instance = testinfra.get_backend(backend)
-    return backend_instance.get_module(_to_pascal_case(module_name))
-
-
-def _to_pascal_case(snake_case):
-    """Convert a snake_case string to its PascalCase equivalent.
-
-    :param snake_case: snake_cased string to be converted
-    :returns: PascalCase string
-    :rtype: str
-
-    """
-    space_case = re.sub('_', ' ', snake_case)
-    wordlist = []
-    for word in space_case.split():
-        wordlist.append(word[0].upper())
-        wordlist.append(word[1:])
-    return ''.join(wordlist)
-
-
-def _to_snake_case(pascal_case):
-    """Convert a PascalCase string to its snake_case equivalent.
-
-    :param pascal_case: PascalCased string to be converted
-    :returns: snake_case string
-    :rtype: str
-
-    """
-    snake_case = re.sub('(^|[a-z])([A-Z])',
-                        lambda match: '{0}_{1}'.format(match.group(1).lower(),
-                                                       match.group(2).lower()),
-                        pascal_case)
-    return snake_case.lower().strip('_')
+    return backend_instance.get_module(snake_to_camel_case(module_name, uppercamel=True))
 
 
 def _get_method_result(module_, module_instance, method_name, method_arg=None):
@@ -297,7 +268,7 @@ def _register_functions():
     can be called via salt.
     """
     try:
-        modules_ = [_to_snake_case(module_) for module_ in modules.__all__]
+        modules_ = [camel_to_snake_case(module_) for module_ in modules.__all__]
     except AttributeError:
         modules_ = [module_ for module_ in modules.modules]
 
diff --git a/salt/states/testinframod.py b/salt/states/testinframod.py
index 7f62dbb901eb..51d2a209872e 100644
--- a/salt/states/testinframod.py
+++ b/salt/states/testinframod.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import, unicode_literals, print_function
 
-import re
 import logging
 
+from salt.utils.stringutils import camel_to_snake_case
+
 log = logging.getLogger(__name__)
 
 try:
@@ -36,24 +37,9 @@ def _module_function_wrapper(name, **methods):
     return _module_function_wrapper
 
 
-def _to_snake_case(pascal_case):
-    """Convert a PascalCase string to its snake_case equivalent.
-
-    :param pascal_case: PascalCased string to be converted
-    :returns: snake_case string
-    :rtype: str
-
-    """
-    snake_case = re.sub('(^|[a-z])([A-Z])',
-                        lambda match: '{0}_{1}'.format(match.group(1).lower(),
-                                                       match.group(2).lower()),
-                        pascal_case)
-    return snake_case.lower().strip('_')
-
-
 def _generate_functions():
     try:
-        modules_ = [_to_snake_case(module_) for module_ in modules.__all__]
+        modules_ = [camel_to_snake_case(module_) for module_ in modules.__all__]
     except AttributeError:
         modules_ = [module_ for module_ in modules.modules]
 
diff --git a/salt/utils/stringutils.py b/salt/utils/stringutils.py
index 50f6bd4f3229..39867ed9ed3e 100644
--- a/salt/utils/stringutils.py
+++ b/salt/utils/stringutils.py
@@ -569,3 +569,39 @@ def get_diff(a, b, *args, **kwargs):
             *args, **kwargs
         )
     )
+
+
+@jinja_filter('to_snake_case')
+def camel_to_snake_case(camel_input):
+    '''
+    Converts camelCase (or CamelCase) to snake_case.
+    From https://codereview.stackexchange.com/questions/185966/functions-to-convert-camelcase-strings-to-snake-case
+
+    :param str camel_input: The camelcase or CamelCase string to convert to snake_case
+
+    :return str
+    '''
+    res = camel_input[0].lower()
+    for i, letter in enumerate(camel_input[1:], 1):
+        if letter.isupper():
+            if camel_input[i-1].islower() or (i != len(camel_input)-1 and camel_input[i+1].islower()):
+                res += '_'
+        res += letter.lower()
+    return res
+
+
+@jinja_filter('to_camelcase')
+def snake_to_camel_case(snake_input, uppercamel=False):
+    '''
+    Converts snake_case to camelCase (or CamelCase if uppercamel is ``True``).
+    Inspired by https://codereview.stackexchange.com/questions/85311/transform-snake-case-to-camelcase
+
+    :param str snake_input: The input snake_case string to convert to camelCase
+    :param bool uppercamel: Whether or not to convert to CamelCase instead
+
+    :return str
+    '''
+    words = snake_input.split('_')
+    if uppercamel:
+        words[0] = words[0].capitalize()
+    return words[0] + ''.join(word.capitalize() for word in words[1:])
diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py
index 48d824e1cc4d..b796e9e030d4 100644
--- a/tests/unit/utils/test_jinja.py
+++ b/tests/unit/utils/test_jinja.py
@@ -981,6 +981,26 @@ def test_sequence(self):
                       .render(data={'foo': 'bar'})
         self.assertEqual(rendered, '1')
 
+    def test_camel_to_snake_case(self):
+        '''
+        Test the `to_snake_case` Jinja filter.
+        '''
+        rendered = render_jinja_tmpl('{{ \'abcdEfghhIjkLmnoP\' | to_snake_case }}',
+                                     dict(opts=self.local_opts, saltenv='test', salt=self.local_salt))
+        self.assertEqual(rendered, 'abcd_efghh_ijk_lmno_p')
+
+    def test_snake_to_camel_case(self):
+        '''
+        Test the `to_camelcase` Jinja filter.
+        '''
+        rendered = render_jinja_tmpl('{{ \'the_fox_jumped_over_the_lazy_dog\' | to_camelcase }}',
+                                     dict(opts=self.local_opts, saltenv='test', salt=self.local_salt))
+        self.assertEqual(rendered, 'theFoxJumpedOverTheLazyDog')
+
+        rendered = render_jinja_tmpl('{{ \'the_fox_jumped_over_the_lazy_dog\' | to_camelcase(uppercamel=True) }}',
+                                     dict(opts=self.local_opts, saltenv='test', salt=self.local_salt))
+        self.assertEqual(rendered, 'TheFoxJumpedOverTheLazyDog')
+
     def test_is_ip(self):
         '''
         Test the `is_ip` Jinja filter.